Skip to content
Astro as components 5 min read

Props to Framework Components

Astro lets you embed React, Vue, Svelte, Solid, and other framework components as interactive islands, and you pass data into them exactly the way you pass attributes to any HTML element. The catch is the server-client boundary: any prop on a hydrated island must survive being serialized to a string, shipped to the browser, and rebuilt there. Understanding what can and cannot cross that boundary is the difference between a working island and a cryptic build error.

How props flow into an island

Inside an .astro file you import a framework component and render it with attributes. Astro forwards those attributes as props to the component, regardless of which framework it comes from.

---
import Counter from "../components/Counter.jsx";
import UserCard from "../components/UserCard.vue";

const user = { id: 7, name: "Ada", admin: true };
---

<Counter client:load start={10} step={2} label="Score" />
<UserCard client:visible user={user} />

Props use JSX-style expression syntax: strings can be quoted (label="Score"), and anything else goes inside curly braces (start={10}, user={user}). This is identical to passing props in React or to passing attributes in Vue/Svelte templates, so the mental model carries over.

Props on framework components are written with {} expressions, not the Astro template’s own attribute rules. A number written as start="10" would arrive as the string "10", so use start={10} when you mean the number.

Static vs. hydrated props

A component rendered without a client:* directive is server-rendered only. Its props are used once during the build to produce HTML and are never sent to the browser, so they can be anything JavaScript can hold during rendering.

The moment you add a hydration directive (client:load, client:visible, client:idle, client:media, or client:only) the props become part of the page payload. Astro serializes them into the HTML so the framework can re-create the component on the client with the same data the server saw.

ScenarioDirectiveProps serialized?
Server render onlynoneNo — used at build time only
Hydrated islandclient:load etc.Yes — sent to the browser
Client-only islandclient:only="react"Yes — sent to the browser

What can be serialized

Astro serializes hydrated props with a JSON-based format (via devalue), which is richer than plain JSON.stringify. It supports the common structured types you reach for in real apps.

---
import Dashboard from "../components/Dashboard.jsx";

const props = {
  title: "Metrics",          // string
  count: 42,                 // number
  enabled: true,             // boolean
  tags: ["a", "b"],          // arrays
  meta: { region: "eu" },    // plain objects (nested OK)
  createdAt: new Date(),     // Date
  ids: new Set([1, 2, 3]),   // Set
  lookup: new Map([["x", 1]]), // Map
  missing: undefined,        // undefined and null
};
---

<Dashboard client:idle {...props} />

These all round-trip safely. Notably, Date, Map, Set, BigInt, RegExp, and undefined are preserved — things plain JSON would mangle or drop.

What cannot be serialized

Anything that is not pure data cannot cross the boundary. If you try, you get a build-time error such as Cannot serialize prop.

Value typeCrosses boundary?Why
Plain data (string, number, object, array)YesPure data
Date, Map, Set, BigInt, RegExpYesSupported by devalue
Functions / callbacksNoCode can’t be serialized
Class instances (with methods)NoMethods are lost
DOM nodes, streams, requestsNoRuntime-only objects
JSX/framework elements as propsNoUse slot / children instead
---
import Form from "../components/Form.jsx";

// This FAILS to build — onSubmit is a function:
// <Form client:load onSubmit={() => save()} />
---

<!-- Instead, let the island own its own handlers -->
<Form client:load action="/api/save" method="POST" />

You cannot pass an event handler or any callback from Astro into a hydrated island. The island runs in the browser where Astro’s server scope no longer exists. Move that logic inside the framework component, or pass plain config (a URL, an id, a flag) and let the component wire up its own behavior.

Passing markup with slots

When you want to nest content rather than data, use slots. Children passed to a framework island become that component’s children (React/Solid), default slot (Vue/Svelte), depending on the framework.

---
import Modal from "../components/Modal.jsx";
---

<Modal client:load title="Confirm">
  <p>This content is server-rendered and slotted into the island.</p>
</Modal>

Named slots also work and map to named slots/props per framework:

<Modal client:load>
  <h2 slot="header">Heading</h2>
  <p>Body content</p>
</Modal>

A complete example

---
import PriceWidget from "../components/PriceWidget.svelte";

const currency = "USD";
const prices = [9.99, 19.99, 49.99];
const updatedAt = new Date();
---

<section>
  <h1>Plans</h1>
  <PriceWidget
    client:visible
    currency={currency}
    prices={prices}
    updatedAt={updatedAt}
  />
</section>

The PriceWidget island hydrates only when scrolled into view, and receives the array, string, and Date intact in the browser.

Output:

[build] generating static routes
[build] PriceWidget hydrated with client:visible
[build] props serialized: currency, prices, updatedAt (Date preserved)

Best Practices

  • Pass plain, serializable data to hydrated islands — strings, numbers, booleans, arrays, and plain objects are the safest currency.
  • Never pass functions or callbacks to a client:* island; keep event handlers inside the framework component and pass URLs/ids/flags instead.
  • Keep prop payloads small: every hydrated prop ships over the wire, so avoid handing an island a giant object when it only needs a few fields.
  • Use slots for markup and props for data — don’t try to pass JSX/template elements as props.
  • Convert class instances to plain objects before passing them, since methods are dropped during serialization.
  • Reach for client:only when a component can’t render on the server, but remember its props are still serialized to the browser.
Last updated June 14, 2026
Was this helpful?