# Astro Islands

Course: [Astro](https://www.devtutorials.ai/learn/astro) · Section: Interactivity
Source: https://www.devtutorials.ai/learn/astro/interactivity/astro-islands

> The islands architecture, adding a UI framework with npx astro add, framework components without a directive, client:load, client:idle, client:visible, client:media and client:only, props and children, sharing state between islands with a store, and when to use a script instead.

## Astro Islands

- Most of a page is just reading: text, pictures, links.
- A few parts must react to clicks: a like button, a search box, a cart.
- Astro sends **zero JavaScript** by default.
- An **island** is one interactive component on an otherwise static page.
- In this lesson, you will add islands built with a UI framework, and choose **when** each one wakes up.

**Simple lesson:** an island is a small interactive part in a sea of plain HTML.

### A Real-World Example

- Asha's post page is plain HTML. It loads fast.
- She wants one **Like** button that counts clicks.
- She does not want to ship JavaScript for the whole page.
- So she writes the button as a Preact component and adds `client:load` to it. Only the button gets JavaScript.

Press **▶ Run**. Click the button. Then read the **Terminal**: only the button became interactive.

<p>Astro sends HTML first. JavaScript only goes where it is needed.</p>
<p>This text never needs JavaScript.</p>

`,
  }}
/>

What happened:

- Astro rendered the button to HTML on the server, like everything else.
- `client:load` told Astro: "also send this component's JavaScript".
- In the browser, Preact **hydrated** the button: it attached the click code to the HTML that was already there.

### What You Will Learn

In this lesson, you will learn:

1. What the islands architecture is.
2. How to add a UI framework such as Preact, React, Vue or Svelte.
3. Why a framework component without a directive sends no JavaScript.
4. The five `client:*` directives and when to use each.
5. How to pass props and children to an island.
6. How islands share state.
7. When to use an island and when a plain `<script>` is enough.

## The Islands Architecture

### Islands in a Sea of HTML

<figure className="ast-fig ngc-fig">
<svg viewBox="0 0 640 230" role="img" aria-labelledby="fig-isl" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-isl">Animation: a page is mostly static HTML. Two small islands, a like button and a search box, light up one after the other as their JavaScript loads.</title>
  <rect x="20" y="16" width="600" height="200" rx="12" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} />
  <text x="40" y="42" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>The page: static HTML (no JavaScript)</text>
  <rect x="40" y="58" width="560" height="22" rx="4" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-slate)" }} />
  <text x="52" y="74" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 11 }}>Header and navigation</text>
  <rect x="40" y="92" width="360" height="104" rx="4" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-slate)" }} />
  <text x="52" y="112" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 11 }}>Blog post text and images</text>
  <rect x="420" y="92" width="180" height="44" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 2 }}>
    <animate attributeName="opacity" values="0.35;0.35;1;1" keyTimes="0;0.3;0.4;1" dur="4s" repeatCount="indefinite" />
  </rect>
  <text x="510" y="119" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>Island: Like button</text>
  <rect x="420" y="152" width="180" height="44" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 2 }}>
    <animate attributeName="opacity" values="0.35;0.35;1;1" keyTimes="0;0.6;0.7;1" dur="4s" repeatCount="indefinite" />
  </rect>
  <text x="510" y="179" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>Island: Search box</text>
</svg>
<figcaption>Blue is plain HTML. Each orange island loads its own small JavaScript, on its own schedule.</figcaption>
</figure>

- The page is the "sea": plain HTML and CSS.
- Each interactive component is an "island".
- Every island loads **on its own**. A slow island does not block the others.
- Many frameworks render the **whole page** in JavaScript. Astro hydrates only the islands.

### Why Islands Are Fast

- Less JavaScript to download.
- Less JavaScript to run on a cheap phone.
- The text is visible before any JavaScript runs.
- You choose **when** each island loads: now, later, or only when it scrolls into view.

Astro components (`.astro` files) never run in the browser. Islands are built with a **UI framework** component, such as Preact, React, Vue, Svelte or Solid.

**Simple lesson:** islands keep the page static and add JavaScript only where it is needed.

## Adding a UI Framework

### Installing an Integration

Add a framework with one command:

```bash
npx astro add preact
```

- The command installs `@astrojs/preact` and `preact`.
- It adds `preact()` to `integrations` in `astro.config.mjs`.
- For React use `npx astro add react`. For Vue, `vue`. For Svelte, `svelte`.
- You may use **more than one** framework on the same site.

This course uses **Preact**. It looks like React (same JSX and hooks) but is about 4 KB. Everything you learn here works the same way with React.

### A Framework Component Without a Directive

- You can use a `.tsx` or `.jsx` component in an `.astro` page, like an Astro component.
- With **no** `client:*` directive, Astro renders it to HTML on the server and sends **no JavaScript**.
- The button below shows, but clicking does nothing.

Press **▶ Run** and click the first button. Nothing happens. The second button works.

        {label}: {count}
      </button>
    );
}
`,
    "src/pages/index.astro": `---
---

<h1>Two counters</h1>
<p>Static HTML only (no directive):</p>

<p>An island (client:load):</p>
`,
  }}
/>

- Open the **HTML** tab. Only the island is wrapped in `<astro-island>`.
- `<astro-island>` is the element Astro uses to find and wake up an island.

### When the Integration Is Missing

- Without the integration, Astro does not know how to render `.tsx` files.
- The build stops with "No valid renderer was found".

Press **▶ Run** to see the error. Then add `integrations: [preact()]` to fix it.

`,
  }}
/>

**Simple lesson:** add the framework with `npx astro add`. A component without `client:*` is only HTML.

## Client Directives

### The Five Directives

<div className="tscroll">

| Directive | When the island wakes up | Good for |
|---|---|---|
| `client:load` | As soon as the page loads | Things needed at once: a menu button |
| `client:idle` | When the browser is free | Less urgent things: a chat widget |
| `client:visible` | When it scrolls into view | Things far down: comments, a footer form |
| `client:media="(query)"` | When a CSS media query matches | Mobile-only parts: a hamburger menu |
| `client:only="preact"` | Only in the browser, never on the server | Code that needs `window` |

</div>

- `client:load` is the most eager. Use it only for things needed **at once**.
- `client:idle` and `client:visible` keep the first load light.
- `client:media` saves JavaScript on screens that do not need the island.
- `client:only` skips server HTML. The island is empty until JavaScript runs.

<figure className="ast-fig ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-dir" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-dir">Animation: a timeline from page load. client:load wakes first, client:idle a little later, client:visible only when the reader scrolls to it.</title>
  <path d="M40 160 H600" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} />
  <text x="40" y="184" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 11 }}>page loads</text>
  <text x="600" y="184" textAnchor="end" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 11 }}>time →</text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 12 }}>
    <circle cx="80" cy="50" r="9" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="opacity" values="0.2;1;1" keyTimes="0;0.1;1" dur="5s" repeatCount="indefinite" /></circle>
    <text x="100" y="54" style={{ fill: "var(--ink)" }}>client:load</text>
    <circle cx="230" cy="90" r="9" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="opacity" values="0.2;0.2;1;1" keyTimes="0;0.3;0.35;1" dur="5s" repeatCount="indefinite" /></circle>
    <text x="250" y="94" style={{ fill: "var(--ink)" }}>client:idle</text>
    <circle cx="450" cy="130" r="9" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="opacity" values="0.2;0.2;1;1" keyTimes="0;0.7;0.75;1" dur="5s" repeatCount="indefinite" /></circle>
    <text x="470" y="134" style={{ fill: "var(--ink)" }}>client:visible</text>
  </g>
  <circle r="5" cy="160" style={{ fill: "var(--ngc-blue)" }}><animate attributeName="cx" values="40;600" dur="5s" repeatCount="indefinite" /></circle>
</svg>
<figcaption>Each island wakes at its own time. Until then it is plain HTML.</figcaption>
</figure>

Press **▶ Run**. Read the **Terminal** to see the order the islands wake up in.

`,
  }}
/>

- The clocks start at slightly different times.
- `WindowWidth` uses `window`, which does not exist on the server. So it needs `client:only`.

Do not add `client:load` to everything "just in case". Each island adds JavaScript. Start with no directive, and add the laziest one that works.

**Simple lesson:** pick the laziest directive that still works: often `client:visible` or `client:idle`.

## Props and Children

### Passing Props

- Pass props to an island like any component: `<Counter start={5} client:load />`.
- Astro **sends the props to the browser** as data, so the island can hydrate with the same values.
- So props must be data: text, numbers, booleans, arrays, plain objects, `Date`, `Map`, `Set`.
- **Functions cannot be passed** to a hydrated island.

### Passing Children

- Content between the tags arrives in the component as `children`.
- That content is rendered by Astro as static HTML. It can even contain Astro components.

Press **▶ Run**. The island gets a number prop and some HTML children.

        <button onClick={() => setOpen(!open)}>
          {open ? "Hide" : "Show"} {title}
        </button>
        {open && <div>{children}</div>}
      </section>
    );
}
`,
    "src/pages/index.astro": `---
const tips = ["Use client:visible for comments", "Keep islands small"];
---

<h1>Islands tips</h1>
    <ul>{tips.map((t) => <li>{t}</li>)}</ul>
`,
  }}
/>

- **[JS]** In `Spoiler.jsx`, leave out `interface Props` and the `: Props` type.

### A Function Prop Breaks the Island

Press **▶ Run**. The build stops, because a function cannot travel to the browser.

<Button onPress={() => console.log("hi")} client:load />
`,
  }}
/>

- Fix: keep the click code **inside** the island.

**Simple lesson:** island props must be plain data. Children arrive as static HTML.

## Sharing State Between Islands

### Islands Are Separate

- Each island is its own small app.
- Two islands cannot read each other's `useState`.
- To share data, both islands import the **same store** module.

### A Tiny Shared Store

- A store holds a value and tells listeners when it changes.
- Real projects often use the small **nanostores** library (`npm install nanostores @nanostores/preact`).
- Below, a 10-line store does the same job, so you can see how it works.

Press **▶ Run**. Click **Add to cart** in one island. The badge in the other island updates.

<h1>Astro sticker</h1>
`,
  }}
/>

- Both islands imported `../stores/cart`, so they share one `count`.
- `useEffect(() => subscribe(setN), [])` re-renders the badge on every change.

With nanostores the same idea is `const cart = atom(0)` in the store, and `useStore(cart)` in each island.

**Simple lesson:** islands share data through a store module that both import.

## Island or Script?

### Choosing the Right Tool

- A `<script>` in an `.astro` file is enough for small behaviour: toggle a class, copy text, open a menu.
- An island is better when the UI has **state** that changes what is drawn: a cart, a filter list, a form with live checks.
- An island is also right when you already have a framework component you want to reuse.

<div className="tscroll">

| Need | Use |
|---|---|
| Toggle a menu open and closed | `<script>` |
| Copy a code block to the clipboard | `<script>` |
| A search box that filters a list as you type | Island |
| A cart shared by the header and the page | Islands + a store |
| A chart library made for React | Island (`client:visible`) |

</div>

Islands render on the server first, so readers see the HTML even before JavaScript loads. Only `client:only` islands start empty.

**Simple lesson:** use a `<script>` for small actions and an island for UI with changing state.

## TL;DR

1. **Island** = one interactive component on a static page. The rest sends no JavaScript.
2. **Framework** = add one with `npx astro add preact` (or react, vue, svelte).
3. **No directive** = server HTML only, no JavaScript.
4. **client:load** = at once. **client:idle** = when the browser is free. **client:visible** = when seen.
5. **client:media** = when a media query matches. **client:only="preact"** = browser only.
6. **Props** = plain data only, no functions. **Children** = static HTML.
7. **Shared state** = a store module both islands import (nanostores in real projects).
8. **Small action** = `<script>`. **Changing UI state** = island.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| No `client:*` directive | A framework component is always interactive | Add a directive | Clicks do nothing |
| `client:load` on every island | Eager is always better | Use `idle` or `visible` when you can | Slower first load |
| `client:` on an `.astro` component | Astro components can hydrate | Use a framework component | The directive does nothing |
| Passing a function prop | Props can be anything | Only data can be sent | Build error |
| Using `window` without `client:only` | The component runs only in the browser | Islands render on the server first | `window is not defined` |
| No framework integration | `.tsx` just works | Run `npx astro add preact` | "No valid renderer was found" |
| Expecting two islands to share `useState` | Islands are one app | Use a shared store | Each island has its own value |

</div>

## Check Yourself

## Exercises

## What to Remember

- An island is one interactive framework component. The rest of the page stays HTML.
- Add a framework with `npx astro add`. No directive means no JavaScript.
- Choose the laziest directive: `visible` or `idle` before `load`. Use `only` for browser-only code.
- Props must be data. Children arrive as HTML.
- Share state between islands with a store module.

Next: **Page Navigation**, where pages swap smoothly without a full reload.
