Type something to search...
What are slots in Astro and how to use named slots?

What are slots in Astro and how to use named slots?

Slots in Astro are placeholders that let a component accept markup from whoever uses it. You put a <slot /> element in the component's template, and any children written between the component's opening and closing tags are rendered in that spot. Named slots let a component accept several separate pieces of markup: the component declares <slot name="footer" />, and the parent sends content to it by adding slot="footer" to a child element. Children without a slot attribute go to the default, unnamed slot.

Slots are what make wrapper components, cards, modals and layouts possible without passing HTML around as strings. They're simple to start with, but there are a few behaviours that surprise people, especially around empty slots and slots passed through several components. In this article you'll learn how the default slot works, how to define and fill named slots, how to provide fallback content, how to check whether a slot was filled with Astro.slots.has(), how to pass several elements to one slot, how to transfer slots between components, and the rules that cause slotted content to disappear.

The Default Slot

A component with a single <slot /> renders all of its children in that position:

---
// src/components/Panel.astro
const { title } = Astro.props;
---
<section class="panel">
  <h2>{title}</h2>
  <slot />
</section>
---
// src/pages/index.astro
import Panel from "../components/Panel.astro";
---
<Panel title="Getting started">
  <p>Install Astro with one command.</p>
  <p>Then run the dev server.</p>
</Panel>

Both paragraphs end up inside the <section>, after the heading. The children can be any mix of HTML, text, expressions and other components, including hydrated framework components.

It helps to compare slots with props:

PropsSlots
What they carryData: strings, numbers, objects, functionsMarkup: elements, text, components
How you pass themAttributes, such as title="Hi"Children between the tags
How you read themAstro.props<slot /> or Astro.slots
Where they renderWherever you use the valueExactly where <slot /> is placed

A good rule: if you're tempted to pass HTML as a string prop, use a slot instead. Props are covered in detail in how to pass props to an Astro component.

Named Slots

When a component needs more than one area of content, give each <slot> a name:

---
// src/components/Card.astro
const { title } = Astro.props;
---
<section class="card">
  <header>
    <slot name="header">
      <h2>{title}</h2>
    </slot>
  </header>
  <div class="card-body">
    <slot />
  </div>
  {Astro.slots.has("footer") && (
    <footer>
      <slot name="footer" />
    </footer>
  )}
</section>

The parent chooses a slot for each child with the slot attribute:

---
// src/pages/index.astro
import Card from "../components/Card.astro";
---
<Card title="Ignored">
  <h2 slot="header">Custom <em>header</em></h2>
  <p>Body text.</p>
  <Fragment slot="footer">
    <a href="/docs">Docs</a>
    <a href="/blog">Blog</a>
  </Fragment>
</Card>

The build output shows where each piece went:

<section class="card"><header><h2>Custom <em>header</em></h2></header><div class="card-body"><p>Body text.</p></div><footer><a href="/docs">Docs</a><a href="/blog">Blog</a></footer></section>

A few things to notice:

  • The slot attribute is removed: the <h2> arrives without slot="header" in the output.
  • Order in the parent doesn't matter: the component decides the order. You could write the footer first and it would still render last.
  • Everything without a slot attribute goes to the default slot: here that's the paragraph. You can also target the default slot explicitly with slot="default", but don't mix the two styles (see the rules at the end of this article).
  • Several children can share one slot name: they're rendered in the order you wrote them.

Passing Several Elements Without a Wrapper

In the example above, the two footer links are wrapped in <Fragment slot="footer">. A Fragment groups children for a slot without adding an element to the HTML. Without it, you'd have to add slot="footer" to each link or wrap them in a <div> that you may not want.

This is especially useful for table parts, where an extra wrapper would produce invalid HTML:

---
// src/components/DataTable.astro
---
<table>
  <thead><slot name="head" /></thead>
  <tbody><slot /></tbody>
</table>
<DataTable>
  <Fragment slot="head">
    <tr><th>Name</th><th>Stock</th></tr>
  </Fragment>
  <tr><td>Boots</td><td>32</td></tr>
  <tr><td>Sneakers</td><td>0</td></tr>
</DataTable>

Fragments have a few other uses, covered in how to use the Fragment component.

Fallback Content

Anything you put between <slot> and </slot> is fallback content, rendered only when the parent sends nothing to that slot. In the Card above, the header slot falls back to an <h2> built from the title prop:

<Card title="Default heading">
  <p>Body text goes into the default slot.</p>
</Card>
<section class="card"><header><h2>Default heading</h2></header><div class="card-body"><p>Body text goes into the default slot.</p></div></section>

Fallbacks work for the default slot too:

---
// src/components/EmptyState.astro
---
<div class="panel">
  <slot>Nothing here yet.</slot>
</div>

Here's an important detail, tested against a real build. These four uses produce different results:

<EmptyState />
<EmptyState></EmptyState>
<EmptyState>{""}</EmptyState>
<EmptyState>{false}</EmptyState>
<div class="panel">Nothing here yet.</div>
<div class="panel">Nothing here yet.</div>
<div class="panel"></div>
<div class="panel"></div>

As soon as the parent writes any child, even an expression that renders nothing, the slot counts as filled and the fallback is skipped. If you want a fallback for "no data", decide in the parent and leave the children out entirely, or check the data inside the component with a prop instead.

Checking Whether a Slot Was Filled

Astro.slots.has(name) returns true if the parent passed content for that slot. Use it to avoid rendering empty wrapper elements:

{Astro.slots.has("footer") && (
  <footer>
    <slot name="footer" />
  </footer>
)}

Without the check, a card with no footer content would still render an empty <footer></footer>, which can add unwanted padding or borders. For the default slot, use Astro.slots.has("default").

You can combine it with props for flexible components, for example rendering an <aside> only when either an aside slot or a note prop is present. Conditional rendering patterns like this are explored in conditional rendering in Astro templates.

Transferring Slots to Another Component

Sometimes a component wraps another component and needs to pass its own slots through. Write a <slot> with both a name and a slot attribute:

---
// src/components/FeatureCard.astro
import Card from "./Card.astro";
const { title } = Astro.props;
---
<Card title={title}>
  <slot name="footer" slot="footer" />
  <slot />
</Card>

name="footer" reads the footer slot given to FeatureCard, and slot="footer" sends it on to Card's footer slot. The plain <slot /> forwards the default slot. This is the same technique used for nesting layouts, where a page layout forwards a head slot to a base layout.

The Transferred Slot Gotcha

There's a catch. When FeatureCard is used without any footer content, Card still sees a footer slot, because FeatureCard always writes <slot name="footer" slot="footer" />. I tested this:

<FeatureCard title="No footer"><p>Main</p></FeatureCard>
<section class="card"><header><h2>No footer</h2></header><div class="card-body"><p>Main</p></div><footer></footer></section>

Astro.slots.has("footer") returned true inside Card, so an empty <footer> appeared. Wrapping the transfer in a condition doesn't help either: the slot is still registered. Practical fixes:

  • Hide empty wrappers with CSS: footer:empty { display: none; } works because Astro's default whitespace handling leaves no text inside the element.
  • Pass a prop instead: have the outer component pass something like hasFooter={Astro.slots.has("footer")} and use that in the inner component.
  • Keep the wrapper in the outer component: put the <footer> element in FeatureCard, where Astro.slots.has() sees the real content.

Rendering a Slot to a String

Astro.slots.render(name) renders a slot to an HTML string. Most of the time <slot /> is all you need, but rendering to a string lets you inspect or transform the content:

---
// src/components/WordCount.astro
const html = await Astro.slots.render("default");
const words = html.replace(/<[^>]+>/g, " ").trim().split(/\s+/).length;
---
<div>
  <slot />
  <p class="meta">{words} words</p>
</div>
<WordCount><p>Three little words</p></WordCount>
<div><p>Three little words</p><p class="meta">3 words</p></div>

render() also accepts an array of arguments, which are passed to a function written as the slot's child. This gives you a render-prop style API:

---
// src/components/Shout.astro
const message = Astro.props.message.toUpperCase();
let html = "";
if (Astro.slots.has("default")) {
  html = await Astro.slots.render("default", [message]);
}
---
<Fragment set:html={html} />
<Shout message="slots!">
  {(message: string) => <div>{message}</div>}
</Shout>
<div>SLOTS!</div>

Keep this for genuinely advanced cases. Rendering slot content to a string and re-inserting it with set:html bypasses some of Astro's normal handling, so test the output carefully, particularly in layouts that also emit styles.

Rules That Make Slotted Content Disappear

If content you passed doesn't show up, it's almost always one of these:

  1. The slot name doesn't exist: <p slot="missing"> is silently dropped if the component has no <slot name="missing" />. There's no warning.
  2. The slot attribute is nested: named slots must be on a direct child of the component. A <p slot="footer"> inside a <div> is not sent to the footer slot; it stays in the <div>, which goes to the default slot with the attribute still on it.
  3. The component has no default slot: children without a slot attribute are dropped if the component only defines named slots.
  4. You're using <> instead of <Fragment>: the short <> syntax can't take attributes, so it can't carry a slot name.
  5. Explicit and implicit default content are mixed: in a test build, a child with slot="default" replaced the other children that had no slot attribute, so only the explicit one rendered. Use one style or the other.
  6. The slot was rendered conditionally: a <slot /> inside a condition that's false doesn't render, which is expected but easy to forget during debugging.

Slots in Framework Components

You can also pass children and named slots from an Astro file to React, Vue, Svelte and other framework components. React, Preact and Solid receive the default slot as children and named slots as props, with kebab-case names converted to camelCase. Vue and Svelte use their own <slot> elements and keep the names as written. The details, along with what can and can't be passed to an island, are in Astro components vs framework components.


FAQ: Astro Slots

Props pass data such as strings, numbers and objects through attributes and are read from Astro.props. Slots pass markup as children and render it wherever the component places a slot element.

Add a name attribute to a slot element in the component, such as slot name="footer", then add slot="footer" to the child element you pass from the parent. Children without a slot attribute go to the default slot.

Wrap them in a Fragment with the slot attribute, for example a Fragment with slot="footer" around several links. The Fragment groups them without adding an extra element to the HTML.

Use Astro.slots.has("name") in the component, or Astro.slots.has("default") for the default slot. It returns true when the parent passed content for that slot, which lets you skip empty wrapper elements.

Fallback content only renders when the parent passes no children for that slot. Even an expression that renders nothing, such as an empty string or false, counts as content and replaces the fallback.

Yes. In the middle component, write a slot element with both name and slot attributes, which reads the incoming slot and forwards it. Be aware that the inner component will then always report that slot as present.


Conclusion

Slots are Astro's way of passing markup into a component. A plain <slot /> receives every child that doesn't ask for somewhere else, named slots let a component expose several distinct areas, and content between <slot> tags acts as a fallback when nothing is passed. Fragment lets you send several elements to one slot without a wrapper, and Astro.slots.has() lets you skip wrapper elements when a slot is empty.

The edge cases are worth remembering: any child, even an empty expression, replaces the fallback; slot names must match exactly and be on direct children; and a slot transferred through a middle component is always seen as present. With those rules in mind, slots give you clean, composable components and are the foundation of every layout you'll build in Astro.

Tags :
Share :

Related Posts

What are Astro Actions and how do they work?

What are Astro Actions and how do they work?

Astro Actions are type-safe backend functions that you define once on the server and then call from the browser as if they were ordinary async fu

Dive Deeper
What are Astro adapters and which one should you choose?

What are Astro adapters and which one should you choose?

An Astro adapter is an integration that builds the server part of your project for a specific runtime, such as Node.js, Vercel Functions, Netlify

Dive Deeper
How to build API endpoints in Astro?

How to build API endpoints in Astro?

To build an API endpoint in Astro, create a .js or .ts file in src/pages/ and export a function named after an HTTP method, such as GET or

Dive Deeper