Astro Islands: Adding React, Vue, or Svelte
A practical guide to the Astro islands architecture — installing a UI framework with astro add, the four client directives, sharing props between server and island, and when an island is actually worth shipping.
What you'll learn
- ✓What the islands architecture is and why it differs from a SPA
- ✓How to add the React integration with npx astro add react
- ✓The four client directives — load, idle, visible, media — and when each fits
- ✓How props pass from an Astro page into a hydrated island
- ✓A simple test for whether an island is worth shipping at all
Prerequisites
- •Comfort with Astro components — see Astro Components and Layouts
- •Some familiarity with React, Vue, or Svelte components
Astro ships zero JavaScript by default. That is wonderful for content but inconvenient for the small interactive pieces a real site needs — a search box, a theme toggle, a like button. Islands are how Astro brings interactivity back without giving up the static baseline.
The islands architecture
A traditional SPA framework ships one JavaScript bundle that boots the whole page. Even a paragraph of text is part of a virtual-DOM tree the browser has to construct and maintain.
The islands model inverts that. The page is mostly static HTML rendered on your machine at build time. Interactivity is opt-in: you place a framework component inside an Astro page and mark it with a client: directive. That component becomes an island — a self-contained interactive widget — and Astro ships only the JavaScript needed for that island. Everything else around it stays as HTML.
Two consequences are worth internalising:
- No global hydration. Each island hydrates independently. If you have three islands on a page, three small bundles run in isolation. None of them know the others exist.
- No JavaScript framework on most pages at all. A page with no islands ships no framework JavaScript. Not a stub, not a runtime — none.
That is the architectural promise.
Step 1: Add a UI framework
Astro supports React, Vue, Svelte, Solid, Preact, Lit, and Alpine through official integrations. Use the framework you already know. We will use React for the examples; the syntax for the others is nearly identical.
npx astro add react
The command edits astro.config.mjs, installs @astrojs/react, react, and react-dom, and sets up the right TypeScript types. After it finishes, the config looks like this:
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
export default defineConfig({
integrations: [react()],
});
For Vue swap react for vue. For Svelte swap it for svelte. The rest of this post applies to all of them.
Step 2: Write a framework component
Create src/components/Counter.jsx:
import { useState } from 'react';
export default function Counter({ start = 0 }) {
const [count, setCount] = useState(start);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
This is ordinary React. There is nothing Astro-specific in the file.
Step 3: Use it inside an Astro page
---
import Counter from '../components/Counter.jsx';
---
<html lang="en">
<body>
<h1>An island demo</h1>
<Counter />
</body>
</html>
Open the page. The button renders — but clicking it does nothing.
That is on purpose. Without a client: directive, Astro renders the React component to HTML on the server at build time and ships no JavaScript. You see the markup; you do not get the behaviour.
To make it interactive, add a directive:
<Counter client:load />
Reload. The button now counts. Astro shipped just enough React and just enough component code to make this one widget work.
The four client directives
Each directive controls when the island hydrates. Pick the one that matches how the visitor will use the component.
client:load
Hydrate immediately when the page loads.
<Counter client:load />
Use for: anything visible above the fold that needs to work as soon as the page is interactive — primary call-to-action buttons, search boxes on a search page.
client:idle
Wait until the browser is idle, then hydrate.
<Counter client:idle />
Use for: nice-to-have widgets above the fold that can wait a few hundred milliseconds — a like button, a small theme toggle.
client:visible
Hydrate only when the component scrolls into view.
<Counter client:visible />
Use for: anything below the fold — comment widgets, embedded calculators, an interactive chart partway down a long post. Most islands on most sites belong here.
client:media
Hydrate only when a CSS media query matches.
<Sidebar client:media="(min-width: 50em)" />
Use for: components that only exist at certain breakpoints — a desktop-only sidebar, a mobile-only menu drawer.
A fifth directive, client:only="react", skips server rendering entirely and renders only on the client. Use it for components that cannot survive server rendering — typically because they read window or localStorage directly on first render.
Passing props to islands
Islands receive props from the Astro page exactly like ordinary components:
---
import Counter from '../components/Counter.jsx';
const initial = 5;
---
<Counter client:load start={initial} />
There is a catch: props must be serializable. Astro turns them into JSON, embeds them in the HTML, and the island reads them when it hydrates. That means strings, numbers, booleans, arrays, plain objects, and null all work. Functions and class instances do not — they cannot survive the trip.
In practice you almost never hit this. The data you usually pass to a widget is configuration: a label, a starting value, an array of items.
Reading data on the server, passing to the island
A common pattern: fetch data in the Astro frontmatter, pass it as props to a hydrated island.
---
import SearchBox from '../components/SearchBox.jsx';
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
const items = posts.map((p) => ({ id: p.id, title: p.data.title }));
---
<SearchBox client:idle items={items} />
items is built at build time. The visitor downloads it inside the HTML, the island hydrates, and the React search component filters in the browser without making any network requests. This is one of Astro’s most useful patterns — pre-compute on the server, hydrate the smallest possible widget on the client.
Try it yourself. Build a ThemeToggle.jsx that toggles a data-theme attribute on document.documentElement. Place it once with client:load in your BaseLayout. Confirm the toggle works on every page without restarting the dev server.
Islands do not share state
This is the single most important constraint to internalise. Two islands on the same page cannot directly share React state. Each one is its own little React tree, rendered into its own root.
If two islands need to coordinate, you have three options:
- Promote them into one larger island. Wrap both pieces in a single React component and hydrate that.
- Use the URL or
localStorageas the shared store. Both islands read and write the same key. - Use
nanostores— a tiny library Astro recommends for cross-island state.
For most sites, the answer is option 1. If two pieces of UI need to talk to each other constantly, they probably belong in the same island anyway.
Mixing frameworks
You can ship more than one UI framework in the same Astro project. A React counter and a Svelte search box on the same page work fine. Each island brings only its own framework runtime.
npx astro add svelte
---
import ReactCounter from '../components/Counter.jsx';
import SvelteSearch from '../components/Search.svelte';
---
<ReactCounter client:load />
<SvelteSearch client:visible />
That said — every framework you add ships its own runtime. Two frameworks is fine. Four is wasteful. Pick the one your team knows and stick with it.
When an island is worth shipping
Islands are a tool, not a default. A useful test before you reach for one:
- Could this be plain HTML and CSS? A
<details>element gives you a working accordion with zero JavaScript. A CSS-only modal works fine for most cases. - Could it be a tiny vanilla script? A theme toggle is twenty lines of plain JS. You do not need React for it.
- Does the user genuinely interact with this? A static chart from
rechartsis not interactive — render it to SVG at build time and ship no JavaScript.
If the answer to all three is “no, it really needs framework-grade interactivity,” reach for an island. Otherwise stay static.
A useful rule of thumb: fewer islands, smaller islands, later hydration. Most sites need three or four islands at most.
Try it yourself. Audit one page of your site. Identify everything dynamic. For each piece, decide whether it is genuinely a hydrated island or whether plain CSS / a <details> / a five-line vanilla script would do. You will usually find half of them do not need an island at all.
Things that commonly trip people up
- Forgot the
client:directive — the component renders but does not behave. Addclient:loador another directive. client:visiblenever fires — the element is hidden withdisplay:none. The IntersectionObserver does not consider hidden elements visible.window is not defined— the component useswindowduring server render. Switch toclient:only="react"or guard withuseEffect.- Two islands “don’t talk to each other” — they are separate React trees. Combine them or share state through
nanostores. - Bundle is bigger than expected — check whether two integrations are pulling in two frameworks. Aim for one.
Recap
You now know:
- The islands architecture renders most of the page as static HTML and hydrates only specific components
npx astro add react(orvue,svelte) installs and wires up the integration in one command- The four directives —
client:load,client:idle,client:visible,client:media— control when an island hydrates - Props pass from the Astro page to the island; they must be JSON-serializable
- Islands are independent React trees and do not share state directly
- Fewer, smaller, later-hydrating islands almost always beats more
Next steps
You now have everything you need to build a real Astro site — static pages, components, layouts, type-safe content collections, and selective interactivity through islands. From here, the natural directions are deployment, view transitions, server endpoints, and the Starlight docs theme. Each is a self-contained step once the foundations in this series are in place.
Questions or feedback? Email codeloomdevv@gmail.com.