Skip to content
C Codeloom
Astro

Astro Components and Layouts

A practical guide to Astro components and layouts — frontmatter, typed props, slots, named slots, and the BaseLayout pattern every Astro project converges on for shared shell HTML.

·8 min read · By Yash Kesharwani
Beginner 11 min read

What you'll learn

  • What an Astro component is and how its two halves work together
  • How to pass props with TypeScript and default values
  • How <slot /> lets a component wrap arbitrary content
  • Named slots for header, footer, and sidebar regions
  • The BaseLayout pattern that every Astro project uses

Prerequisites

An Astro component is the unit you will spend almost all of your time writing. This post covers what a component is, how props and slots work, and the layout pattern every Astro project converges on.

What an Astro component is

An Astro component is a file ending in .astro with two halves:

  • Frontmatter — JavaScript or TypeScript between --- fences, run at build time
  • Template — HTML below the fences, with {expressions} for values and <Component /> tags for children
---
const greeting = "Hello";
---
<p>{greeting}, world!</p>

Components live in src/components/. Files there do not become routes — only files in src/pages/ do. A component renders only when something imports and uses it.

Importing and using a component

Create src/components/Greeting.astro:

---
const message = "Welcome to Astro";
---
<p class="greeting">{message}</p>

<style>
  .greeting {
    font-weight: 600;
    color: rebeccapurple;
  }
</style>

Use it from a page:

---
import Greeting from '../components/Greeting.astro';
---
<html lang="en">
  <body>
    <Greeting />
  </body>
</html>

Two things to notice. First, the import path includes .astro — unlike React’s .jsx which is usually omitted. Second, the <style> block is scoped to the component. Other .greeting selectors elsewhere will not collide.

Props

A static greeting is dull. Real components accept props.

---
const { name } = Astro.props;
---
<p>Hello, {name}!</p>

Use it like an HTML element with attributes:

<Greeting name="Ada" />
<Greeting name="Linus" />

Astro.props is the magic object containing whatever attributes the parent passed. Destructure what you need at the top of the frontmatter.

Typed props

Astro encourages you to type props with a TypeScript interface. The convention is to name it Props:

---
interface Props {
  name: string;
  emoji?: string;
}

const { name, emoji = "👋" } = Astro.props;
---
<p>{emoji} Hello, {name}!</p>

A few things this gives you:

  • VS Code autocompletes the prop names when you use the component
  • Missing required props show a red squiggle in the editor
  • Default values work like any JavaScript destructure default

Typed props are not enforced at runtime — they are a build-time check. Still, the editor experience is excellent and worth keeping on.

Passing dynamic values

Props can be any JavaScript expression, not only strings:

---
const tags = ["astro", "beginner"];
---
<PostHeader title="Hello" tags={tags} publishedAt={new Date()} />

Anything inside { } is JavaScript. Strings without { } are literals — name="Ada" and name={"Ada"} are equivalent.

Slots

Components often need to wrap arbitrary content. Astro’s mechanism for this is <slot />.

---
interface Props {
  title: string;
}
const { title } = Astro.props;
---
<article class="card">
  <h2>{title}</h2>
  <div class="body">
    <slot />
  </div>
</article>

Use it with children inside the tags:

<Card title="React vs Astro">
  <p>This is the body of the card.</p>
  <p>Multiple paragraphs work fine.</p>
</Card>

Whatever sits between the opening and closing tags is rendered where <slot /> appears. This is the same idea as React’s children prop, just with a tag instead of a magic prop name.

Named slots

For more complex layouts you can have multiple slots, each with a name:

---
interface Props {
  title: string;
}
const { title } = Astro.props;
---
<article>
  <header><slot name="header" /></header>
  <h2>{title}</h2>
  <div class="body"><slot /></div>
  <footer><slot name="footer" /></footer>
</article>

The parent fills each slot with slot="name":

<Card title="Post">
  <span slot="header">Published Monday</span>
  <p>Default slot content goes here.</p>
  <span slot="footer">By Ada</span>
</Card>

The unnamed <slot /> catches everything else.

Fallback content

A slot can have default content used when the parent does not provide any:

<slot name="footer">
  <span>No footer provided.</span>
</slot>

Try it yourself. Build a Callout.astro component with a type prop ("info" or "warning") and a default slot. Use it twice on a page with different types and different inner content. Confirm the styles change based on type.

Layout components

A layout component is just a normal .astro component that wraps the shared shell of every page — <html>, <head>, navigation, footer. Conventionally they live in src/layouts/.

Create src/layouts/BaseLayout.astro:

---
interface Props {
  title: string;
  description?: string;
}
const { title, description = "A site built with Astro" } = Astro.props;
---
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content={description} />
    <title>{title}</title>
  </head>
  <body>
    <header>
      <a href="/">Home</a>
      <a href="/about">About</a>
      <a href="/blog">Blog</a>
    </header>
    <main>
      <slot />
    </main>
    <footer>
      <small>© {new Date().getFullYear()} My Site</small>
    </footer>
  </body>
</html>

Use it from any page:

---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Home" description="Welcome to my Astro site">
  <h1>Welcome</h1>
  <p>This page uses the shared layout.</p>
</BaseLayout>

The page only describes its own content. The <html>, <head>, navigation, and footer come from the layout. Add a new page? Wrap it in BaseLayout and the entire shell is consistent automatically.

This is the pattern in Astro. Every project has at least one base layout. Larger projects often have PostLayout, DocsLayout, MarketingLayout — each wrapping BaseLayout and adding their own structure.

Layouts nesting layouts

Layouts compose. A PostLayout wraps BaseLayout and adds a sidebar:

---
import BaseLayout from './BaseLayout.astro';

interface Props {
  title: string;
  publishedAt: Date;
}
const { title, publishedAt } = Astro.props;
---
<BaseLayout title={title}>
  <article>
    <header>
      <h1>{title}</h1>
      <time datetime={publishedAt.toISOString()}>
        {publishedAt.toDateString()}
      </time>
    </header>
    <slot />
  </article>
  <aside>Sidebar content here.</aside>
</BaseLayout>

A blog post page picks the more specific layout:

---
import PostLayout from '../layouts/PostLayout.astro';
---
<PostLayout title="Hello" publishedAt={new Date("2026-06-16")}>
  <p>The body of the post lives here.</p>
</PostLayout>

The same wrapping idea repeats indefinitely. There is no special syntax for layouts — they are normal components with <slot />.

Conditional rendering

Astro templates are not JSX, but they accept the same JavaScript expressions.

---
const { isAdmin } = Astro.props;
---
{isAdmin && <p class="admin">Admin tools available.</p>}

{isAdmin ? <a href="/admin">Admin</a> : <a href="/login">Sign in</a>}

For more complex logic, compute the result in the frontmatter and interpolate a variable:

---
const { user } = Astro.props;
let message;
if (!user) {
  message = <p>Please sign in.</p>;
} else if (user.admin) {
  message = <p>Welcome, {user.name} (admin).</p>;
} else {
  message = <p>Welcome, {user.name}.</p>;
}
---
<header>{message}</header>

Plain JavaScript runs in the frontmatter exactly like Node.

Rendering lists

Map an array to elements inside the template:

---
const tags = ["astro", "beginner", "components"];
---
<ul>
  {tags.map((tag) => <li>{tag}</li>)}
</ul>

No key prop is needed. Astro renders the array once on the server — there is no virtual DOM to reconcile across renders.

Component conventions

A few habits worth picking up from day one.

  • PascalCase filenamesCard.astro, BaseLayout.astro. The variable name you import with should match.
  • One component per file. Astro will not stop you doing otherwise, but every project does this.
  • Co-locate styles with <style> blocks inside the component file. Scoped styles avoid collisions.
  • Type props with an interface Props at the top of the frontmatter. The editor experience makes it worth it.
  • Layouts in src/layouts/, everything else in src/components/.

Try it yourself. Convert one of your existing pages to use BaseLayout. Move the <html>, <head>, and navigation into the layout. Confirm the page renders identically. Then create a second page that also uses BaseLayout. Notice how little code is left in each page.

Recap

You now know:

  • An Astro component is a .astro file with frontmatter and a template
  • Props arrive on Astro.props and are best typed with an interface Props
  • <slot /> lets a component wrap arbitrary children — like React’s children
  • Named slots support header, footer, and sidebar regions
  • A layout component is a normal .astro component that wraps the shared HTML shell
  • Layouts compose — PostLayout can wrap BaseLayout and so on

Next steps

So far every page has hard-coded content. The next post turns that around with content collections — a type-safe way to manage Markdown and MDX with a schema, validation, and full TypeScript autocomplete in your templates.

→ Next: Astro Content Collections

Questions or feedback? Email codeloomdevv@gmail.com.