
How to pass props to an Astro component?
To pass props to an Astro component, write them as attributes when you use the component, such as <Greeting name="Sajjad" />, and read them inside the component from the Astro.props object, usually by destructuring: const { name } = Astro.props;. Strings can be passed in quotes, and any other JavaScript value (numbers, arrays, objects, dates, even functions) goes inside curly braces, like price={39} or tags={["a", "b"]}. You can give props default values while destructuring and describe them with a TypeScript Props interface so your editor and astro check catch mistakes.
Props are how one component configures another, so they're the first thing you reach for once a page is split into pieces. Astro's version will feel familiar if you've used JSX, but there are a few differences worth knowing, because Astro components run once on the server and never in the browser. In this article you'll learn the basic syntax, every way of passing a value, how defaults and boolean props behave, how to spread objects, how to forward HTML attributes to an underlying element, and where props come from on pages.
The Basic Syntax
Here's a minimal component that accepts two props:
---
// src/components/Greeting.astro
const { name, greeting = "Hello" } = Astro.props;
---
<p>{greeting}, {name}!</p>
And here it is being used from a page:
---
// src/pages/index.astro
import Greeting from "../components/Greeting.astro";
const user = { name: "Ada" };
---
<Greeting name="Sajjad" />
<Greeting name={user.name} greeting="Welcome back" />
<Greeting name={`${user.name} Lovelace`} />
That page builds to:
<p>Hello, Sajjad!</p><p>Welcome back, Ada!</p><p>Hello, Ada Lovelace!</p>
Three things are happening:
- Attributes become props: every attribute you write on
<Greeting>ends up as a key onAstro.propsinside the component. - Destructuring is just JavaScript:
Astro.propsis a plain object, soconst { name } = Astro.propsis ordinary destructuring. You can also readAstro.props.namedirectly. - Expressions use curly braces: anything inside
{}is evaluated in the parent's component script before being handed to the child.
The component script between the --- fences runs on the server at build time (or on each request for on-demand pages), which is covered in more depth in the post on the Astro component script.
Every Way to Pass a Value
Unlike HTML attributes, which are always strings, Astro props keep their JavaScript type. The table below shows the common forms:
| You write | The component receives |
|---|---|
title="Hello" | The string "Hello" |
count={3} | The number 3 |
count="3" | The string "3" |
featured | The boolean true |
featured={false} | The boolean false |
tags={["astro", "css"]} | An array |
author={{ name: "Ada" }} | An object (outer braces for the expression, inner for the object) |
label={`Hi ${name}`} | A template literal string |
format={formatPrice} | A function |
The difference between count={3} and count="3" matters. To confirm it, a tiny debug component that prints its props as JSON helps:
---
// src/components/Debug.astro
const props = Astro.props;
---
<pre>{JSON.stringify(props)}</pre>
<Debug count={3} count2="3" flag on={false} />
{"count":3,"count2":"3","flag":true,"on":false}
A bare attribute like flag arrives as true, a quoted value is always a string, and a braced value keeps its type.
Passing Functions
Because Astro components render on the server, you can pass functions as props to another Astro component and call them during rendering. This is handy for formatting:
---
// src/components/ProductCard.astro
interface Props {
name: string;
price: number;
tags?: string[];
featured?: boolean;
formatPrice?: (value: number) => string;
}
const {
name,
price,
tags = [],
featured = false,
formatPrice = (value) => `£${value.toFixed(2)}`,
} = Astro.props;
---
<article class:list={["card", { featured }]}>
<h3>{name}</h3>
<p>{formatPrice(price)}</p>
{tags.length > 0 && (
<ul>
{tags.map((tag) => <li>{tag}</li>)}
</ul>
)}
</article>
---
// src/pages/shop.astro
import ProductCard from "../components/ProductCard.astro";
const toEuro = (value: number) => `€${value.toFixed(2)}`;
---
<ProductCard name="Notebook" price={4.5} featured />
<ProductCard name="Desk lamp" price={39} formatPrice={toEuro} />
The first card renders £4.50 with a featured class, and the second renders €39.00.
This only works between Astro components. Functions can't be passed to a hydrated framework component (one with a client:* directive), because Astro has to serialise those props into the HTML and functions can't survive that trip. The difference is covered in Astro components vs framework components.
Default Values
Defaults are set with normal destructuring syntax. A default applies when the prop is missing or explicitly undefined:
---
// src/components/Avatar.astro
const {
src,
alt = "",
size = 48,
rounded = true,
} = Astro.props;
---
<img
src={src}
alt={alt}
width={size}
height={size}
class:list={{ rounded }}
/>
A few details catch people out:
- null is not undefined: passing
size={null}skips the default and gives younull. - An empty string is a value:
alt=""keeps the empty string, which is exactly what you want for decorative images. - Defaults aren't visible from outside: the caller only learns about them if you document them, which is one reason to add a
Propstype.
Spreading an Object as Props
When you already have an object whose keys match the component's props, spread it instead of listing each one:
---
// src/pages/shop.astro
import ProductCard from "../components/ProductCard.astro";
const product = { name: "Desk lamp", price: 39, tags: ["lighting", "office"] };
---
<ProductCard {...product} />
This is common when looping over data from JSON, an API or a content collection:
{products.map((product) => <ProductCard {...product} />)}
Spread and explicit attributes can be mixed. When the same key appears twice, the one written later wins, so <ProductCard {...product} price={0} /> overrides the price.
Spreading is convenient but has a cost: every key in the object is passed, including ones the component doesn't expect. TypeScript still checks that the required props are present with the right types, but it doesn't complain about extra keys that arrive through a spread (in testing, spreading an object with an unknown sku key passed astro check cleanly). If a component forwards its remaining props to an element, those extra keys end up in your HTML, so spread deliberately. Looping over arrays like this is covered in depth in how to loop over arrays in Astro.
Forwarding HTML Attributes
Many components wrap a single HTML element, such as a button or a link, and should accept any attribute that element accepts. The pattern is to destructure the props you handle and collect the rest with ...attrs, then spread them onto the element:
---
// src/components/Button.astro
import type { HTMLAttributes } from "astro/types";
interface Props extends HTMLAttributes<"button"> {
variant?: "primary" | "secondary";
}
const { variant = "primary", class: className, ...attrs } = Astro.props;
---
<button class:list={["btn", `btn-${variant}`, className]} {...attrs}>
<slot />
</button>
<Button type="submit" class="wide" aria-label="Save changes" disabled>Save</Button>
<Button variant="secondary" data-track="cancel">Cancel</Button>
Built output:
<button class="btn btn-primary wide" type="submit" aria-label="Save changes" disabled>Save</button><button class="btn btn-secondary" data-track="cancel">Cancel</button>
Some points to note:
- Rename class when destructuring:
classis a reserved word in JavaScript, soconst { class } = Astro.propsis a syntax error. Writeclass: classNameinstead. - Merge rather than overwrite: if you spread
attrsafter settingclass, a caller'sclasswould replace yours. Pulling it out and merging withclass:listkeeps both. - HTMLAttributes gives autocomplete:
HTMLAttributes<"button">fromastro/typesincludes every valid button attribute, so callers get suggestions and type errors for typos. - Hyphenated names work as-is:
data-trackandaria-labelarrive as keys with hyphens, which is why spreading them onto an element just works.
Props Versus Slots
Props carry data. Slots carry markup. If you find yourself passing a string of HTML as a prop, or a prop called content, a slot is usually the better tool:
<!-- Awkward: markup squeezed into a prop -->
<Card title="Hello" body="<p>Some <em>text</em></p>" />
<!-- Better: markup passed as children -->
<Card title="Hello">
<p>Some <em>text</em></p>
</Card>
The child elements go wherever the component places a <slot /> element. Slots, including named slots and fallbacks, are covered in what slots are and how to use named slots.
Typing Props with TypeScript
Astro looks for a type or interface called Props in the component script and uses it to check every place the component is used:
---
// src/components/Greeting.astro
interface Props {
name: string;
greeting?: string;
}
const { greeting = "Hello", name } = Astro.props;
---
<p>{greeting}, {name}!</p>
Now <Greeting /> without name is an error in your editor and in astro check. Two things to keep in mind:
- astro build doesn't type-check: a typo in a prop name won't fail the build on its own. Add
astro checkto your build script if you want it to. - The interface doesn't need exporting: Astro finds it by name, though exporting it is allowed.
Unions, HTMLAttributes, ComponentProps and polymorphic components are covered in how to type component props with TypeScript.
Where Pages Get Their Props
Components get props from their parent. Pages in src/pages/ have no parent, so Astro.props on a page is filled in a different way. On a dynamic route, getStaticPaths() can return a props object for each path:
---
// src/pages/posts/[id].astro
export function getStaticPaths() {
return [
{ params: { id: "1" }, props: { author: "Blu" } },
{ params: { id: "2" }, props: { author: "Erika" } },
];
}
const { id } = Astro.params;
const { author } = Astro.props;
---
<h1>Post {id} by {author}</h1>
Markdown files that use the layout frontmatter property also pass props: the layout receives the file's frontmatter, headings and url on Astro.props. Dynamic routes are covered in dynamic routes with getStaticPaths.
Props Are Not Reactive
Props are read once, when the component renders. Changing a variable after the template has rendered does nothing, and there's no state or re-rendering in an Astro component. This trips up developers coming from React or Vue:
---
// src/components/Counter.astro
let { start = 0 } = Astro.props;
---
<!-- Renders the starting value only. Clicking does nothing. -->
<button>{start}</button>
If a value has to change in the browser, you need either a <script> tag that updates the DOM or a framework component with a client:* directive. Astro components produce HTML; anything interactive happens after that HTML reaches the browser.
Common Mistakes
When a prop doesn't seem to arrive, check these first:
- Quoted numbers and booleans:
count="3"is a string anddisabled="false"is the string"false", which is truthy. Use braces:count={3},disabled={false}. - Missing braces around objects:
author={ name: "Ada" }is a syntax error. Objects need double braces:author={{ name: "Ada" }}. - Destructuring class: use
class: className, neverclasson its own. - Props on HTML elements versus components: objects and functions passed to a plain HTML element are converted to strings. Only components receive real JavaScript values.
- camelCase versus kebab-case:
<Card itemCount={3} />gives youitemCount, while<Card item-count={3} />gives you"item-count"as the key. Pick one style for component props and stick with it; camelCase destructures more cleanly.
FAQ: Astro Component Props
Read them from Astro.props in the component script, usually by destructuring the title, date and any other props you need. The object contains every attribute written on the component where it was used.
Yes. Put the value in curly braces, such as a count of 3 or open set to false. A value in quotes is always a string, and an attribute with no value, such as featured, is passed as true.
Use a default in the destructuring assignment, for example giving size a default of 48. The default applies when the prop is missing or undefined, but not when it's null.
Yes, between Astro components, because they all render on the server. You can't pass functions to a framework component that's hydrated with a client directive, since its props must be serialised into the page.
Destructure the props you use and collect the rest with a rest pattern, gathering everything else into an attrs object, then spread the rest object onto the element. Type them with HTMLAttributes from astro/types.
Astro components render once to HTML and have no client-side runtime, so props aren't reactive. Use a script tag or a hydrated framework component for anything that has to change in the browser.
Conclusion
Passing props in Astro comes down to writing attributes on a component and reading them from Astro.props. Strings go in quotes, every other value goes in curly braces and keeps its type, bare attributes become true, and destructuring gives you defaults for free. Spreading objects and forwarding the rest of the props to an element keep wrapper components short and flexible.
Because Astro components run on the server, you can pass anything JavaScript can hold, including functions, but nothing you pass is reactive. Add a Props interface so mistakes show up in your editor and in astro check, use slots rather than props for markup, and reach for a script or a framework island when a value has to change after the page loads.


