Skip to content
Astro as components 4 min read

Nesting & Composing Components

Composition is the heart of building maintainable interfaces in Astro. Instead of writing one enormous template, you assemble pages from small, focused components that nest inside one another and pass content down through slots. Because Astro renders components to static HTML by default, this nesting carries zero runtime cost — no matter how deep your tree goes, the browser receives plain markup unless you explicitly opt into an island.

Nesting Astro components

Any .astro component can import and render another component in its template. Treat them like HTML elements: import the file in the component script, then use it as a tag with a capitalized name.

---
// src/components/Card.astro
const { title } = Astro.props;
---
<article class="card">
  <h2>{title}</h2>
  <slot />
</article>
---
// src/pages/index.astro
import Card from "../components/Card.astro";
import Badge from "../components/Badge.astro";
---
<main>
  <Card title="Getting started">
    <Badge label="New" />
    <p>Read the quickstart to ship your first page.</p>
  </Card>
</main>

The <slot /> inside Card.astro is where the nested children — the <Badge> and <p> — get projected. This is the primary mechanism for passing content into a wrapper component.

Passing children through slots

A default <slot /> captures everything between a component’s opening and closing tags. You can also define named slots to project content into specific positions, which is ideal for layouts with multiple regions.

---
// src/components/Layout.astro
---
<div class="shell">
  <header><slot name="header" /></header>
  <section class="body"><slot /></section>
  <footer><slot name="footer">© 2026 DevCraftly</slot></footer>
</div>
---
// src/pages/about.astro
import Layout from "../components/Layout.astro";
---
<Layout>
  <h1 slot="header">About us</h1>
  <p>The main content lands in the default slot.</p>
  <small slot="footer">All rights reserved.</small>
</Layout>

The footer slot above includes fallback content (© 2026 DevCraftly) that renders only when no slot="footer" is supplied. Named slots can sit anywhere in the consumer markup; Astro reorders them into the right place at build time.

Slot patternSyntax in parentUse case
Default slot<slot />Single block of children
Named slot<slot name="header" />Distinct layout regions
Fallback content<slot>Default text</slot>Optional content with a default

Tip: To forward all received children straight through to a nested component, render <slot /> inside that child’s tag — Astro will pass the projected content along the chain.

Composing with framework components

Astro composes seamlessly across UI frameworks. You can nest a React, Vue, Svelte, or Solid component inside an Astro component and hydrate only the pieces that need interactivity using a client:* directive. Everything else stays static HTML.

---
// src/pages/dashboard.astro
import Card from "../components/Card.astro";
import Counter from "../components/Counter.jsx";
import Chart from "../components/Chart.svelte";
---
<Card title="Live metrics">
  <Counter client:load />
  <Chart client:visible data-src="/api/stats" />
</Card>

Here Card is a static Astro wrapper. Counter hydrates immediately with client:load, while Chart waits until it scrolls into view with client:visible. The static shell ships instantly and the islands wake up independently.

Warning: Astro components cannot be nested inside a framework component’s children when that framework component is hydrated — only other framework components of the same framework can be passed as children to a hydrated island. Keep .astro wrappers on the outside.

Building reusable layout primitives

Composition shines when you extract layout primitives — Stack, Grid, Container — and combine them. Each accepts children via <slot /> and forwards styling props.

---
// src/components/Stack.astro
const { gap = "1rem" } = Astro.props;
---
<div class="stack" style={`--gap:${gap}`}>
  <slot />
</div>

<style>
  .stack { display: flex; flex-direction: column; gap: var(--gap); }
</style>
---
import Stack from "../components/Stack.astro";
import Card from "../components/Card.astro";
const items = ["Plan", "Build", "Ship"];
---
<Stack gap="2rem">
  {items.map((item) => <Card title={item}><p>{item} phase details.</p></Card>)}
</Stack>

Output:

<div class="stack" style="--gap:2rem">
  <article class="card"><h2>Plan</h2><p>Plan phase details.</p></article>
  <article class="card"><h2>Build</h2><p>Build phase details.</p></article>
  <article class="card"><h2>Ship</h2><p>Ship phase details.</p></article>
</div>

Mapping over data to render nested components is fully supported in the template and produces the static HTML above — no client JavaScript involved.

Best Practices

  • Keep components small and single-purpose; compose them rather than adding props to a monolith.
  • Reach for named slots when a component has more than one content region, and provide fallback content for optional slots.
  • Keep static .astro wrappers on the outside and hydrate only the leaf islands that truly need interactivity.
  • Choose the lightest client:* directive that works — client:visible or client:idle over client:load when immediate interactivity isn’t required.
  • Extract layout primitives (Stack, Grid, Container) so spacing and structure stay consistent across pages.
  • Forward children with a bare <slot /> to build flexible wrapper chains without duplicating markup.
Last updated June 14, 2026
Was this helpful?