Astro components (.astro files) are the foundation of any Astro project. They are HTML-only templating components with no client-side runtime.
Component Structure
An Astro component consists of two main parts: the component script (JavaScript/TypeScript) and the component template (HTML).
---
// Component Script (optional)
// JavaScript/TypeScript code runs at build time
import Header from './Header.astro';
const greeting = 'Hello, world!';
---
<!-- Component Template -->
<!-- HTML with JSX-like expressions -->
<div>
<Header />
<h1>{greeting}</h1>
</div>
The component script is enclosed in code fences (---) and runs during the build. This code never reaches the browser.
Component Script
The component script runs at build time on the server. You can:
- Import other components
- Import data from other files
- Fetch data from APIs or databases
- Create variables to use in your template
src/components/BlogPost.astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { getEntry } from 'astro:content';
// Fetch data at build time
const post = await getEntry('blog', Astro.params.slug);
const { Content } = await post.render();
// Process data
const readingTime = calculateReadingTime(post.body);
const publishedDate = new Date(post.data.pubDate).toLocaleDateString();
---
<BaseLayout title={post.data.title}>
<article>
<h1>{post.data.title}</h1>
<p>Published: {publishedDate} • {readingTime} min read</p>
<Content />
</article>
</BaseLayout>
Everything in the component script runs once at build time, not on every request.
Props
Components accept data through props, similar to React or Vue:
src/components/Card.astro
---
const { title, description, href } = Astro.props;
---
<article class="card">
<h3>{title}</h3>
<p>{description}</p>
<a href={href}>Read more</a>
</article>
Use it:
---
import Card from './components/Card.astro';
---
<Card
title="First Post"
description="This is my first blog post"
href="/blog/first-post"
/>
TypeScript Props
Define prop types with TypeScript:
src/components/Card.astro
---
interface Props {
title: string;
description: string;
href: string;
image?: string;
}
const { title, description, href, image } = Astro.props;
---
<article class="card">
{image && <img src={image} alt={title} />}
<h3>{title}</h3>
<p>{description}</p>
<a href={href}>Read more</a>
</article>
Basic Props
Typed Props
Default Values
---
const { name, age } = Astro.props;
---
<p>{name} is {age} years old</p>
---
interface Props {
name: string;
age: number;
}
const { name, age } = Astro.props;
---
<p>{name} is {age} years old</p>
---
interface Props {
name: string;
role?: string;
}
const { name, role = 'Guest' } = Astro.props;
---
<p>{name} - {role}</p>
Slots
Slots allow you to pass child content to components:
src/components/Card.astro
---
const { title } = Astro.props;
---
<div class="card">
<h2>{title}</h2>
<div class="content">
<slot />
</div>
</div>
Use it:
<Card title="My Card">
<p>This content goes in the slot!</p>
<button>Click me</button>
</Card>
Named Slots
Define multiple slots with names:
src/components/BlogPost.astro
<article>
<header>
<slot name="header" />
</header>
<main>
<slot /> <!-- default slot -->
</main>
<footer>
<slot name="footer" />
</footer>
</article>
Use named slots:
<BlogPost>
<h1 slot="header">My Blog Post</h1>
<p>This goes in the default slot.</p>
<p>Multiple elements work too!</p>
<p slot="footer">Written by Alice</p>
</BlogPost>
Fallback Content
Provide default content if no slot content is passed:
<div class="card">
<slot>
<p>No content provided</p>
</slot>
</div>
Slot Implementation
From the source code at src/runtime/server/render/slot.ts, slots are rendered as functions that return template results:
export function renderSlot(
result: SSRResult,
slotted: ComponentSlotValue | RenderTemplateResult,
fallback?: ComponentSlotValue | RenderTemplateResult,
): RenderInstance {
if (!slotted && fallback) {
return renderSlot(result, fallback);
}
return {
async render(destination) {
await renderChild(
destination,
typeof slotted === 'function' ? slotted(result) : slotted
);
},
};
}
The Astro Global
Every Astro component has access to the global Astro object:
---
// Route parameters from dynamic routes
const { id } = Astro.params;
// Component props
const { title } = Astro.props;
// Request information
const url = Astro.url;
const headers = Astro.request.headers;
---
Common Properties
Utilities
Astro.props // Component props
Astro.params // Route parameters
Astro.request // Request object
Astro.url // Request URL
Astro.site // Site URL from config
Astro.slots // Slot utilities
Astro.redirect() // Redirect to another page
Astro.slots.has() // Check if slot has content
Astro.cookies // Cookie helpers
Framework Components
Astro components can render framework components from React, Vue, Svelte, and more:
---
import ReactCounter from './ReactCounter.jsx';
import VueButton from './VueButton.vue';
import SvelteCard from './SvelteCard.svelte';
---
<div>
<!-- By default, these render as static HTML -->
<ReactCounter />
<VueButton />
<!-- Add client directive for interactivity -->
<SvelteCard client:load />
</div>
Framework components are static by default. Add a client directive to make them interactive.
Component Rendering
From src/runtime/server/render/component.ts, Astro determines how to render each component:
export function renderComponent(
result: SSRResult,
displayName: string,
Component: unknown,
props: Record<string | number, any>,
slots: ComponentSlots = {},
): RenderInstance | Promise<RenderInstance> {
// Astro components
if (isAstroComponentFactory(Component)) {
return renderAstroComponent(result, displayName, Component, props, slots);
}
// Framework components (React, Vue, etc.)
return renderFrameworkComponent(result, displayName, Component, props, slots);
}
Practical Examples
src/components/Button.astro
---
interface Props {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
href?: string;
}
const { variant = 'primary', size = 'md', href } = Astro.props;
const Tag = href ? 'a' : 'button';
---
<Tag
class={`btn btn-${variant} btn-${size}`}
href={href}
>
<slot />
</Tag>
<style>
.btn {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
font-weight: 600;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-sm { font-size: 0.875rem; }
.btn-md { font-size: 1rem; }
.btn-lg { font-size: 1.125rem; }
</style>
Data-Fetching Component
src/components/UserProfile.astro
---
interface Props {
userId: string;
}
const { userId } = Astro.props;
// Fetch data at build time
const response = await fetch(`https://api.example.com/users/${userId}`);
const user = await response.json();
---
<div class="profile">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
<a href={`mailto:${user.email}`}>Contact</a>
</div>
Conditional Rendering
src/components/Alert.astro
---
interface Props {
type: 'info' | 'warning' | 'error' | 'success';
title?: string;
}
const { type, title } = Astro.props;
const icons = {
info: '💡',
warning: '⚠️',
error: '❌',
success: '✅',
};
---
<div class={`alert alert-${type}`}>
<span class="icon">{icons[type]}</span>
<div class="content">
{title && <h4>{title}</h4>}
<slot />
</div>
</div>
Best Practices
Keep components focused
Each component should have a single responsibility.
Use TypeScript
Type your props for better developer experience and fewer bugs.
Extract reusable logic
Move complex logic to utility functions in the component script.
Use slots for flexibility
Slots make components more reusable and composable.
Default to Astro components
Only use framework components when you need client-side interactivity.
Learn More
Islands Architecture
Add interactivity with client directives
Layouts
Create reusable page structures