Slots
A component is far more reusable when the caller can decide what goes inside it. Astro’s <slot /> element is the mechanism for that: it marks the spot where any child content passed to a component gets rendered. If you have used children in React or ng-content in Angular, the idea is identical — Astro just borrows the standard Web Components <slot> name. Because slots are resolved at build time on the server, they add zero JavaScript to the page.
What a slot is
When you render a component with content between its opening and closing tags, that content is the component’s children. The <slot /> element inside the component’s template is the placeholder that renders those children. Whatever the consumer writes between the tags is projected into the slot, in the same order, at the same place.
---
// src/components/Card.astro
interface Props {
title: string;
}
const { title } = Astro.props;
---
<article class="card">
<h2>{title}</h2>
<div class="card-body">
<slot />
</div>
</article>
A page or another component can now fill the card with arbitrary markup:
---
// src/pages/index.astro
import Card from "../components/Card.astro";
---
<Card title="Getting started">
<p>This paragraph is the child content.</p>
<a href="/docs">Read the docs</a>
</Card>
The <p> and <a> land exactly where the <slot /> sits, producing a single static HTML document.
Output:
<article class="card">
<h2>Getting started</h2>
<div class="card-body">
<p>This paragraph is the child content.</p>
<a href="/docs">Read the docs</a>
</div>
</article>
Slots versus props
Props and slots solve different problems, and most components use both. Reach for the right tool:
| Pass with | Best for | Example |
|---|---|---|
| Props | Scalar configuration and data | title, variant, href |
| Slots | Arbitrary rich markup / children | A paragraph, a list, nested components |
A good rule: if the value is a string, number, or object the component reasons about, make it a prop. If it is markup the component should display verbatim, make it a slot.
Fallback content
A <slot /> can wrap default content. If the consumer provides children, they replace the fallback; if they pass nothing, the fallback renders instead. This is perfect for sensible defaults.
---
// src/components/Panel.astro
---
<section class="panel">
<slot>
<p>No content provided.</p>
</slot>
</section>
<Panel /> <!-- renders: No content provided. -->
<Panel><p>Hi!</p></Panel> <!-- renders: Hi! -->
Tip: Fallback content is a quiet way to make a component robust. A layout’s
<slot><DefaultHero /></slot>guarantees a page always has something in the hero region, even before an author fills it in.
Slots in layouts
Layouts are the most common home for the default slot — it is where each page’s unique body is injected into the shared shell.
---
// src/layouts/BaseLayout.astro
interface Props {
title: string;
}
const { title } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{title}</title>
</head>
<body>
<main>
<slot />
</main>
</body>
</html>
Everything a page renders between <BaseLayout> and </BaseLayout> flows into that single <slot />.
Checking whether a slot was filled
Inside the component script you can inspect Astro.slots to branch on whether content was actually passed. Astro.slots.has("default") returns a boolean, which is useful for conditionally rendering wrapper markup only when there is something to wrap.
---
// src/components/Aside.astro
const hasContent = Astro.slots.has("default");
---
{hasContent && (
<aside class="callout">
<slot />
</aside>
)}
You can also render a slot to a string with await Astro.slots.render("default"), which is handy when you need to process the projected HTML before output. For typical usage, placing <slot /> directly in the template is all you need.
Passing dynamic children
Because children are evaluated by the consumer, you can pass expressions, mapped lists, or even islands into a slot. The component projecting them does not need to know anything about their shape.
---
// src/pages/team.astro
import Card from "../components/Card.astro";
const members = ["Ada", "Linus", "Grace"];
---
<Card title="Team">
<ul>
{members.map((name) => <li>{name}</li>)}
</ul>
</Card>
An interactive island can be projected the same way — <Card title="Live"><Counter client:visible /></Card> — and the client:* directive keeps JavaScript scoped to just that island, preserving Astro’s zero-JS-by-default model for the rest of the card.
Best practices
- Use the default
<slot />for the one main content region; reach for named slots when you need multiple distinct insertion points. - Provide fallback content inside
<slot>…</slot>so a component degrades gracefully when no children are passed. - Decide deliberately between props and slots: scalar config goes in props, arbitrary markup goes in slots.
- Use
Astro.slots.has("default")to avoid rendering empty wrapper elements when a slot is unused. - Keep components dumb about their children — let the consumer pass expressions, lists, and islands without the component coupling to their structure.
- Remember slots resolve on the server at build time, so they cost zero client-side JavaScript.