Layouts, Slots & Component Composition

Chapter 4
Layouts, Slots & Component Composition
Shared page shells, the default and named <slot />, and nesting layouts

Every page written so far has repeated its own <html>/<head>/<body> boilerplate. A layout is just an ordinary Astro component, conventionally kept in src/layouts/, that wraps a page's own content instead.

A Basic Layout

// src/layouts/BaseLayout.astro --- interface Props { title: string; } const { title } = Astro.props; --- <html lang="en"> <head> <title>{title}</title> </head> <body> <slot /> </body> </html>
// src/pages/index.astro --- import BaseLayout from '../layouts/BaseLayout.astro'; --- <BaseLayout title="Home"> <p>Page content goes here.</p> </BaseLayout>

Whatever's placed between <BaseLayout> and </BaseLayout> in the page renders wherever <slot /> appears inside the layout itself. title is passed as an ordinary prop, same as any other component — a layout isn't a special kind of file, just a component used in this particular role by convention.

Astro's slots are the real, native mechanism — not a framework invention
Astro's <slot /> is modeled directly on the native browser Web Component slotting API — the same underlying concept the HTML platform itself provides. React's children is just a plain prop with no dedicated syntax; Vue's own <slot> element is closer in spirit to Astro's own, both drawing from the same native idea.

Named Slots

// src/layouts/BaseLayout.astro (body) <body> <aside> <slot name="sidebar" /> </aside> <main> <slot /> </main> </body>
<BaseLayout title="Home"> <p slot="sidebar">Sidebar content</p> <p>Main content — goes into the default slot</p> </BaseLayout>

An element with a slot="name" attribute is routed into the matching named <slot name="..." />; everything else falls into the unnamed default slot.

Nesting Layouts

// src/layouts/BlogPostLayout.astro --- import BaseLayout from './BaseLayout.astro'; interface Props { title: string; publishDate: string; } const { title, publishDate } = Astro.props; --- <BaseLayout title={title}> <p class="date">{publishDate}</p> <slot /> </BaseLayout>

A layout can wrap another layout — BlogPostLayout adds blog-specific chrome (a publish date) around its own <slot />, while still passing through to BaseLayout for the shared <html> shell. Layouts compose the same way any other Astro components do.

Slots, Compared Across Frameworks

FrameworkMechanism
ReactThe children prop — a plain JavaScript value, no dedicated syntax
Vue<slot>, named via <template #name>
Svelte 5Snippet props rendered via {@render children()} — replaced the older <slot> mechanism used in Svelte 3/4
Astro<slot /> and named <slot name="..." />, modeled on the native Web Component API

Coding Challenges

Challenge 1

Build BaseLayout.astro with a full HTML shell, a title prop for the <title> tag, and a default <slot />, then use it from index.astro.

📄 View solution
Challenge 2

Add a named "sidebar" slot to BaseLayout, and pass sidebar content into it from a page using slot="sidebar", alongside default-slot main content.

📄 View solution
Challenge 3

Build a BlogPostLayout.astro that wraps BaseLayout and adds a publishDate prop rendered above its own <slot />, then use it from a blog post page.

📄 View solution

Chapter 4 Quick Reference

  • src/layouts/ — an ordinary component used by convention to wrap a page's own content
  • <slot /> — the default insertion point, modeled on the native Web Component slotting API
  • <slot name="..." /> / slot="..." — named slots for multiple distinct insertion points
  • Layouts are just components — they take props and can wrap other layouts
  • Genuine cross-framework difference — React's plain children prop vs. Vue/Astro's native-style <slot> vs. Svelte 5's newer snippet-based {@render}
  • Next chapter: Content Collections