Markdown source
Components Markdown source
Readable source view for humans. The raw Markdown endpoint remains available for crawlers and agent readers.
---
title: "Components"
description: "Reusable building blocks — like Python functions that return HTML."
kind: guide
maturity: evergreen
confidence: high
origin: ai-drafted
author: "Agent"
directedBy: "krow"
tags: [astro, fundamentals]
published: 2026-03-15
modified: 2026-05-31
wordCount: 1060
readingTime: 5
series: "learn-astro"
series_order: 4
prerequisites: [astro-files]
related: [astro-styling]
url: https://krowdev.com/guide/astro-components/
---
## Agent Context
- Canonical: https://krowdev.com/guide/astro-components/
- Markdown: https://krowdev.com/guide/astro-components.md
- Full corpus: https://krowdev.com/llms-full.txt
- Kind: guide
- Maturity: evergreen
- Confidence: high
- Origin: ai-drafted
- Author: Agent
- Directed by: krow
- Published: 2026-03-15
- Modified: 2026-05-31
- Words: 1060 (5 min read)
- Tags: astro, fundamentals
- Series: learn-astro (#4)
- Prerequisites: astro-files
- Related: astro-styling
- Content map:
- h2: Components Are Functions
- h2: Defining a Component
- h2: Using a Component
- h2: Props = Function Arguments
- h2: Real Example: krowdev's Header
- h2: Slots — The "Body" of a Component
- h2: krowdev's Component Map
- h2: Sources
- Diagrams: Mermaid fences are paired with adjacent ASCII companions in this document (1 Mermaid, 1 ASCII); HTML figures expose rendered SVG plus copyable Mermaid/ASCII source tabs.
- Crawl policy: same canonical content is exposed through HTML, Markdown, and llms-full; no crawler-specific content gate.
## Components Are Functions
This assumes you understand [.astro files](/guide/astro-files/) — components are just `.astro` files that take props. Continue with [layouts](/guide/astro-layouts/), then [styling](/guide/astro-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.
:::analogy
```python
# Python function
def badge(label: str, color: str = "gray") -> str:
return f'<span class="badge badge-{color}">{label}</span>'
# Usage
html = badge("beginner", color="green")
```
An Astro component is the same idea, but with `.astro` file syntax instead of a Python function.
:::
## Defining a Component
```astro
---
// src/components/Badge.astro
// 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
```astro
---
// In any other .astro file
import 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:
```html
<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 |
:::key
`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:
<p class="cv-label">src/components/Header.astro</p>
<nav class="cv-tabs">
<button class="cv-tab active" data-tab="source">Source</button>
<button class="cv-tab" data-tab="compiled">Compiled</button>
<button class="cv-tab" data-tab="rendered">Rendered</button>
</nav>
<div class="cv-panel active" data-panel="source">
```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>
{navLinks.map(link => (
<a href={link.href}
class:list={['nav-link', { active: isActive(link.href) }]}>
{link.label}
</a>
))}
<ThemeToggle />
</nav>
</header>
```
<div class="cv-panel" data-panel="compiled">
```html
<!-- 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>
</nav>
</header>
```
<div class="cv-panel" data-panel="rendered">
Notice what happened at build time:
- **`navLinks.map(...)`** expanded to three `<a>` tags
- **`isActive(link.href)`** evaluated to `true` for Guides (we're on a guide page), adding the `active` class
- **`<ThemeToggle />`** was replaced by its full HTML output (button + SVGs)
- **`class:list`** resolved the conditional class to a plain `class` string
- **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:
```astro
---
// src/components/Card.astro
interface Props {
title: string;
}
const { title } = Astro.props;
---
<h3>{title}</h3>
<slot /> <!-- Child content goes here -->
```
Usage:
```astro
<Card title="Getting Started">
<p>This paragraph becomes the slot content.</p>
<p>So does this one.</p>
</Card>
```
:::analogy
`<slot />` is like `*args` or a callback in Python:
```python
def card(title: str, body: str) -> str:
return f'<h3>{title}</h3>{body}'
```
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:
```mermaid
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"]
```
```ascii
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:
- `type` prop: `"info"`, `"warning"`, or `"tip"`
- Slot content for the body
Expected usage:
```astro
<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**
```astro
---
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 />
<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>
```
---
Previous: [.astro Files](/guide/astro-files/) | Next: [Layouts](/guide/astro-layouts/)
## Sources
- Astro Docs, [Components](https://docs.astro.build/en/basics/astro-components/)
- Astro Docs, [Component props](https://docs.astro.build/en/basics/astro-components/#component-props)
- Astro Docs, [Slots](https://docs.astro.build/en/basics/astro-components/#slots)