Components
Reusable building blocks — like Python functions that return HTML.
On this page
Components Are Functions
This assumes you understand .astro files — components are just .astro files that take props. Continue with layouts, then styling which is how component CSS is scoped.
A component is an .astro file that you import and use inside other .astro files. It accepts inputs (props) and returns HTML.
# Python functiondef badge(label: str, color: str = "gray") -> str: return f'<span class="badge badge-{color}">{label}</span>'
# Usagehtml = badge("beginner", color="green")An Astro component is the same idea, but with .astro file syntax instead of a Python function.
Defining a Component
---// Define accepted props (like function parameters)interface Props { label: string; color?: string; // ? means optional}
// Destructure with defaults (like keyword args)const { label, color = 'gray' } = Astro.props;---
<span class={`badge badge-${color}`}>{label}</span>
<style> .badge { font-size: 0.7rem; padding: 0.15rem 0.5rem; border-radius: 4px; font-family: var(--font-mono), monospace; }</style>Using a Component
---// In any other .astro fileimport Badge from '../components/Badge.astro';---
<!-- Use it like an HTML tag --><Badge label="beginner" color="green" /><Badge label="concept" /><Badge label="advanced" color="red" />Output:
<span class="badge badge-green">beginner</span><span class="badge badge-gray">concept</span><span class="badge badge-red">advanced</span>Props = Function Arguments
| Python | Astro |
|---|---|
def func(x: str) | interface Props { x: string } |
def func(x: str = "hi") | const { x = "hi" } = Astro.props |
func(x="hello") | <Component x="hello" /> |
| Type error at runtime | Type error at build time |
Astro.props is the only way a component receives data from its parent. There’s no global state, no context magic. Data flows down through props — explicit and traceable.
Real Example: krowdev’s Header
The site header is a component that uses other components. Here’s the full lifecycle:
src/components/Header.astro
---import ThemeToggle from './ThemeToggle.astro';
const pathname = Astro.url.pathname;const navLinks = [ { href: '/explore/', label: 'Explore' }, { href: '/guide/', label: 'Guides' }, { href: '/article/', label: 'Articles' },];
function isActive(href) { return pathname.startsWith(href);}---
<header class="header"> <nav class="header-inner"> <a href="/" class="logo"> <span class="logo-mark">k</span> <span class="logo-text">krowdev</span> </a> <div class="nav-right"> {navLinks.map(link => ( <a href={link.href} class:list={['nav-link', { active: isActive(link.href) }]}> {link.label} </a> ))} <ThemeToggle /> </div> </nav></header><!-- Compiled output for a wiki page (simplified) --><header class="header" data-astro-cid-3ef6ksr2> <nav class="header-inner" data-astro-cid-3ef6ksr2> <a href="/" class="logo" data-astro-cid-3ef6ksr2> <span class="logo-mark" data-astro-cid-3ef6ksr2>k</span> <span class="logo-text" data-astro-cid-3ef6ksr2>krowdev</span> </a> <div class="nav-right" data-astro-cid-3ef6ksr2> <a href="/explore/" class="nav-link" data-astro-cid-3ef6ksr2> Explore </a> <a href="/guide/" class="nav-link active" data-astro-cid-3ef6ksr2> Guides </a> <a href="/article/" class="nav-link" data-astro-cid-3ef6ksr2> Articles </a> <!-- ThemeToggle component expanded inline --> <button class="theme-toggle" aria-label="Toggle theme"> <svg class="icon-sun">...</svg> <svg class="icon-moon">...</svg> </button> </div> </nav></header>Notice what happened at build time:
navLinks.map(...)expanded to three<a>tagsisActive(link.href)evaluated totruefor Guides (we’re on a guide page), adding theactiveclass<ThemeToggle />was replaced by its full HTML output (button + SVGs)class:listresolved the conditional class to a plainclassstring- Scoped
data-astro-cid-*attributes were added for CSS isolation
The component imported another component (ThemeToggle), computed which nav link is active, and mapped an array to HTML — all at build time.
Slots — The “Body” of a Component
Sometimes you want a component to wrap other content, not just receive props. That’s what <slot /> does:
---interface Props { title: string;}const { title } = Astro.props;---
<div class="card"> <h3>{title}</h3> <slot /> <!-- Child content goes here --></div>Usage:
<Card title="Getting Started"> <p>This paragraph becomes the slot content.</p> <p>So does this one.</p></Card><slot /> is like *args or a callback in Python:
def card(title: str, body: str) -> str: return f'<div class="card"><h3>{title}</h3>{body}</div>'Except instead of passing HTML as a string argument, you put it between the opening and closing tags.
krowdev’s Component Map
Here’s how the actual components in krowdev relate:
Base.astro composes a content slot, Header (logo, nav, and the client-side ThemeToggle), and Footer. KBEntry extends Base and adds SeriesSidebar, an article slot, and TableOfContents.
graph TD
accTitle: How krowdev's components relate
accDescr: Base.astro composes a content slot, Header (logo, nav, and the client-side ThemeToggle), and Footer. KBEntry extends Base and adds SeriesSidebar, an article slot, and TableOfContents.
base["Base.astro (layout)"]
base --> bslot["<slot /> — page content"]
base --> header["Header.astro"]
header --> nav["logo + nav links"]
header --> theme["ThemeToggle.astro — only client-side JS"]
base --> footer["Footer.astro"]
kb["KBEntry.astro (extends Base)"]
kb --> sidebar["SeriesSidebar.astro — reads series entries"]
kb --> kbslot["<slot /> — article content"]
kb --> toc["TableOfContents.astro — reads headings"]
Base.astro (layout)
├── <slot /> ← page content goes here
├── Header.astro
│ ├── logo + nav links
│ └── ThemeToggle.astro ← the only client-side JS
└── Footer.astro
KBEntry.astro (layout, extends Base)
├── SeriesSidebar.astro ← reads series entries, builds nav
├── <slot /> ← article content
└── TableOfContents.astro ← reads headings array
Every component does one thing. Data flows down through props. No hidden dependencies.
Challenge: Build a component
Create a Callout.astro component that renders a styled box with a label and slot content.
It should accept:
typeprop:"info","warning", or"tip"- Slot content for the body
Expected usage:
<Callout type="tip"> <p>This is a helpful tip!</p></Callout>Try writing it in src/components/Callout.astro — define the interface, destructure props, use <slot />, add scoped styles.
One possible solution
---interface Props { type: 'info' | 'warning' | 'tip';}const { type } = Astro.props;const labels = { info: 'Info', warning: 'Warning', tip: 'Tip' };---
<div class={`callout callout-${type}`}> <span class="label">{labels[type]}</span> <slot /></div>
<style> .callout { padding: 1rem; border-left: 3px solid; border-radius: 0 8px 8px 0; margin: 1rem 0; } .callout-info { border-color: var(--ctp-blue); } .callout-warning { border-color: var(--ctp-yellow); } .callout-tip { border-color: var(--ctp-green); } .label { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; }</style>Sources
- Astro Docs, Components
- Astro Docs, Component props
- Astro Docs, Slots