Components & Props
Everything about .astro components — frontmatter, templates, props, slots, and scoped styles.
Anatomy of a component
Every Astro component is a .astro file split into two parts:
---
// ── Frontmatter (server-side script) ──────────────────────
// Runs on the server. Can import, fetch, compute — anything.
// Result variables are available in the template below.
const message = "Hello from the server!"
---
<!-- ── Template (HTML) ──────────────────────────────────── -->
<!-- Uses JSX-like {} syntax to inject server values -->
<p>{message}</p>
Key rule: code in --- fences never runs in the browser.
It’s removed entirely before the page is sent to the user.
Defining and using props
Props let parent components pass data into a child:
---
// Button.astro — child component
interface Props {
label: string
href?: string
variant?: 'primary' | 'outline'
}
const { label, href = '#', variant = 'primary' } = Astro.props
---
<a href={href} class={`btn btn-${variant}`}>{label}</a>
Use it in a parent:
---
import Button from './Button.astro'
---
<Button label="Get Started" href="/signup" variant="primary" />
<Button label="Learn More" href="/docs" variant="outline" />
Slots
Slots let you pass HTML content into a component — like React’s children:
---
// Card.astro
---
<div class="card">
<slot /> <!-- default slot — renders whatever the parent passes -->
</div>
Use it:
<Card>
<h2>My Title</h2>
<p>Some content inside the card.</p>
</Card>
Named slots
For multiple injection points, name them:
---
// Modal.astro
---
<div class="modal">
<header><slot name="title" /></header>
<main><slot /></main>
<footer><slot name="actions" /></footer>
</div>
<Modal>
<span slot="title">Confirm Delete</span>
<p>Are you sure? This cannot be undone.</p>
<div slot="actions">
<button>Cancel</button>
<button class="danger">Delete</button>
</div>
</Modal>
Scoped styles
Add a <style> block in your component and Astro automatically scopes it —
styles only apply to that component, never leak out:
<h1 class="title">Hello</h1>
<style>
/* This ONLY applies to .title in THIS file */
.title {
color: coral;
font-size: 2rem;
}
</style>
For global styles use <style is:global>:
<style is:global>
/* Applies everywhere */
body { margin: 0; }
</style>
Client-side scripts
Add a <script> tag for browser JavaScript:
<button id="counter">Count: 0</button>
<script>
// This DOES run in the browser
const btn = document.getElementById('counter')!
let count = 0
btn.addEventListener('click', () => {
count++
btn.textContent = `Count: ${count}`
})
</script>
Astro bundles scripts automatically. Multiple components with <script> blocks
get their scripts deduplicated and bundled together.
Expressions in templates
Inside the HTML template you can use any JavaScript expression inside {}:
---
const isLoggedIn = true
const items = ['Apple', 'Banana', 'Cherry']
const color = 'orange'
---
<!-- Conditional rendering -->
{isLoggedIn ? <p>Welcome back!</p> : <a href="/login">Sign in</a>}
<!-- Short-circuit (render if true) -->
{isLoggedIn && <p>You are logged in.</p>}
<!-- List rendering -->
<ul>
{items.map(item => <li>{item}</li>)}
</ul>
<!-- Dynamic class -->
<div class={`text-${color}`}>Colored text</div>
<!-- class:list utility for conditional classes -->
<button
class:list={[
'btn',
{ 'btn-active': isLoggedIn },
color === 'orange' && 'btn-orange',
]}
>
Click me
</button>
Fragments
Astro components can return multiple top-level elements — no wrapping <div> needed:
---
// MultiReturn.astro — works fine, no wrapper required
---
<h1>Title</h1>
<p>Paragraph one</p>
<p>Paragraph two</p>
If you need an explicit fragment for template logic, use <Fragment> or <>:
{condition && (
<>
<p>First paragraph</p>
<p>Second paragraph</p>
</>
)}