---
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["&lt;slot /&gt; — 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["&lt;slot /&gt; — 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)