Component Props
Props are how a parent component passes data down to a child in Astro. Any HTML-like attribute you put on a component tag becomes a property the child reads from the Astro.props global. Because the component script runs on the server, props are resolved at build time (or per request in SSR) and never need to be hydrated — so a well-typed prop interface gives you editor autocompletion, build-time safety, and zero client-side cost. This page covers defining props, typing them, supplying defaults, and the patterns that keep components reusable.
Reading props with Astro.props
When you render a component and pass attributes, Astro collects them into a single object available as Astro.props inside the child’s component script. Destructure it to pull out the values you need.
---
// src/components/Greeting.astro
const { name, role } = Astro.props;
---
<p>Hello {name}, you are signed in as {role}.</p>
A parent passes props exactly like HTML attributes — string literals as plain text, anything else inside {} expressions:
---
// src/pages/index.astro
import Greeting from "../components/Greeting.astro";
const currentUser = { role: "admin" };
---
<Greeting name="Ada" role={currentUser.role} />
Prop names are case-sensitive and passed through verbatim. Unlike React, Astro does not camelCase or rename your attributes, so
aria-labelstaysaria-labeland a prop namedisActivemust be read asisActive.
Typing props with a Props interface
Astro gives a special meaning to a TypeScript type or interface named exactly Props: it uses it to type Astro.props automatically and to type-check the attributes callers pass in. This is the single most valuable habit when authoring components.
---
// src/components/ProductCard.astro
interface Props {
name: string;
price: number;
tags?: string[];
}
const { name, price, tags } = Astro.props;
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(price);
---
<article class="card">
<h2>{name}</h2>
<p>{formatted}</p>
{tags && <ul>{tags.map((t) => <li>{t}</li>)}</ul>}
</article>
Now an editor will autocomplete name, price, and tags on the call site, and the build fails if a required prop is missing or a type is wrong:
<ProductCard name="Keyboard" price="forty" />
Output:
Type 'string' is not assignable to type 'number'.
Property 'price' on <ProductCard> expected number, got string.
You can use a type alias instead of an interface, and you can compose existing types — for example extending the native HTML attributes of an element:
---
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"a"> & {
variant?: "primary" | "ghost";
};
const { variant = "primary", ...rest } = Astro.props;
---
<a class={`btn btn-${variant}`} {...rest}><slot /></a>
Providing default values
Because props are just a destructured object, you supply defaults with standard JavaScript destructuring defaults. Mark the corresponding interface members optional with ?.
---
interface Props {
title: string;
level?: 1 | 2 | 3;
align?: "left" | "center";
}
const { title, level = 2, align = "left" } = Astro.props;
const Tag = `h${level}` as const;
---
<Tag style={`text-align:${align}`}>{title}</Tag>
Here <Heading title="About" /> renders an <h2> aligned left, while <Heading title="Hero" level={1} align="center" /> overrides both. Defaults keep the call site clean and document the component’s sensible baseline.
Forwarding and spreading props
A common pattern is to accept arbitrary extra attributes and forward them to the root element using rest syntax and the spread operator. This makes wrapper components feel like native elements.
---
interface Props {
label: string;
[key: string]: unknown;
}
const { label, ...attrs } = Astro.props;
---
<button {...attrs}>{label}</button>
Now <Action label="Save" id="save-btn" disabled /> forwards id and disabled straight onto the <button>.
Props reference
| Concept | How it works |
|---|---|
| Read props | const { x } = Astro.props; |
| Type props | interface Props { x: string } |
| Optional prop | x?: string in the interface |
| Default value | const { x = "fallback" } = Astro.props; |
| Required prop | non-optional interface member |
| Forward extras | const { ...rest } = Astro.props; then {...rest} |
| Pass to framework | <Counter client:load start={3} /> |
Props passed to a framework island (React, Vue, Svelte) must be JSON-serializable when hydrated with a
client:*directive — functions, class instances, and DOM nodes cannot cross the server-to-client boundary.
Best practices
- Always declare an
interface Propsso callers and the build catch shape mismatches early. - Mark optional props with
?and pair them with destructuring defaults for a clean, self-documenting API. - Use union literal types (
"primary" | "ghost") instead of free-form strings to constrain valid values. - Forward unknown attributes with
...restand{...rest}to make wrapper components behave like native elements. - Keep props serializable when passing them to hydrated framework components — no functions or class instances across islands.
- Read props once at the top of the script and derive computed values there, leaving the template for presentation only.