Skip to content
Astro as islands 4 min read

Islands & Hydration

Astro renders your entire page to static HTML on the server and ships zero JavaScript by default. When you need interactivity — a search box, a cart counter, a carousel — you opt in to it locally with an island: a self-contained framework component that Astro hydrates on the client independently of the rest of the page. This “islands architecture” is the core of why Astro sites stay fast: the static shell loads instantly, and only the interactive bits cost any JS.

What an island actually is

An island is any UI framework component (React, Preact, Vue, Svelte, Solid, or even a raw .astro component) that you mark for client-side hydration. Everything outside the island remains inert HTML. The page is rendered once on the server; each island then becomes a tiny isolated app that boots on its own, with its own JS bundle, on its own schedule.

The key mental model: static by default, interactive by exception. You are not shipping a single-page application that takes over the whole document. You are sprinkling small interactive widgets into a sea of HTML.

---
// src/pages/index.astro
import Counter from '../components/Counter.jsx';
import Header from '../components/Header.astro';
---
<html lang="en">
  <body>
    <Header />            <!-- static HTML, no JS -->
    <main>
      <h1>Welcome</h1>
      <Counter client:load />   <!-- an interactive island -->
    </main>
  </body>
</html>

Here Header ships no JavaScript at all. Counter is an island: its HTML is rendered on the server, and because of client:load its JS is also sent and hydrated in the browser.

Enabling a UI framework

Islands require a framework integration. Add one with the CLI, which wires up the integration and installs peer dependencies for you.

npx astro add react

Output:

✔ Resolving packages...
  Astro will add the following dependency: @astrojs/react
✔ Continue? … yes
✔ Adding integrations...
✔ Successfully added the following integration to your project: @astrojs/react

This updates astro.config.mjs:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';

export default defineConfig({
  integrations: [react()],
});

You can mix frameworks freely — a Vue island and a React island can coexist on the same page, each with its own runtime, because each is hydrated in isolation.

The client:* directives

A component only becomes an island when you add a client:* directive. The directive controls when and whether hydration happens. Without one, the component is rendered to HTML and stays static.

DirectiveWhen it hydratesTypical use
client:loadImmediately on page loadCritical above-the-fold UI
client:idleWhen the browser is idleLow-priority widgets
client:visibleWhen it scrolls into viewBelow-the-fold components
client:mediaWhen a media query matchesMobile-only / desktop-only UI
client:onlySkips server render; client onlyComponents that can’t SSR
---
import Carousel from '../components/Carousel.svelte';
import Newsletter from '../components/Newsletter.vue';
---
<Carousel client:visible />
<Newsletter client:idle />

Tip: Choose the laziest directive that still feels instant to the user. client:visible and client:idle defer JS execution, keeping your initial load lean. Reserve client:load for things that must be interactive the moment the page paints.

How hydration works under the hood

When Astro builds the page it does three things for each island:

  1. Renders the component to HTML on the server and inlines that markup.
  2. Emits a small loader script and the component’s JS bundle.
  3. Wraps the island in an <astro-island> custom element carrying the props and the directive.

The loader watches for the directive’s trigger (load, idle, intersection, media match). When it fires, it dynamically imports the framework runtime plus the component and hydrates only that island. Other islands sit untouched until their own triggers fire. This is why a page with ten islands does not download one giant bundle — each island’s JS is requested independently.

Passing props and children

Islands receive serializable props from the surrounding .astro file. Props are JSON-serialized into the <astro-island> element, so pass plain data — not functions or class instances.

---
import StarRating from '../components/StarRating.jsx';
const product = { id: 'sku-42', rating: 4.5 };
---
<StarRating client:visible value={product.rating} max={5} />

You can also pass static HTML as children via slots. The slotted content is rendered on the server and handed to the island as children, so even the markup inside an island can be zero-JS until hydration.

Islands vs. a full SPA

The contrast with a traditional single-page app is the whole point.

AspectIslands (Astro)Full SPA
Default JSNoneEntire app bundle
Initial HTMLFully renderedOften empty shell
Hydration scopePer-componentWhole app at once
Time to interactiveFast, incrementalBlocked on full bundle

Gotcha: State is not shared between islands automatically. Two React islands are two separate React apps. To share state, use nano stores, the URL, localStorage, or custom events — not a single top-level context provider.

Best Practices

  • Keep islands small and focused; hydrate widgets, not entire layouts.
  • Prefer client:visible or client:idle over client:load unless interactivity must be immediate.
  • Pass only serializable props — no functions, class instances, or DOM nodes.
  • Use .astro components (which are always zero-JS) for anything that doesn’t need interactivity.
  • Reserve client:only for components that genuinely cannot render on the server, since it sacrifices SSR HTML and SEO content.
  • Share cross-island state with lightweight stores or browser events rather than a global framework provider.
Last updated June 14, 2026
Was this helpful?