Photo by chansu shin on Unsplash

On this page

This chapter follows Content collections in Astro.

What is Astro introduced islands as interactive components in a static page. This chapter is the mechanics: how to bring in a UI framework, how to choose when each island loads, and what you give up at the boundary between server and browser.

Add a UI framework

Astro components cover static markup and small scripts. For stateful UI, add a framework integration:

Terminal window
npx astro add react

Official integrations exist for React, Preact, Svelte, Vue, Solid, and Alpine.js. Several can coexist in one project, although each one adds its own runtime to the pages that use it.

src/components/Counter.tsx
import { useState } from 'react';
export default function Counter({ start = 0 }: { start?: number }) {
const [count, setCount] = useState(start);
return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>;
}

Import it into an .astro file like any component. Without a directive it renders to HTML on the server, and the button does nothing in the browser.

Client directives

A client:* directive turns the component into an island and sets when it hydrates:

Directive Hydrates Typical use
client:load Immediately on page load Above-the-fold controls the reader needs at once
client:idle When the browser is idle (requestIdleCallback) Search boxes, menus, lower-priority widgets
client:visible When the element enters the viewport Charts, comments, anything below the fold
client:media="(query)" When a media query matches A mobile-only navigation drawer
client:only="react" Only in the browser, never rendered on the server Components that need window during render
src/pages/index.astro
---
import Counter from '../components/Counter.tsx';
import Comments from '../components/Comments.tsx';
import MobileNav from '../components/MobileNav.tsx';
---
<MobileNav client:media="(max-width: 50em)" />
<Counter client:load start={5} />
<Comments client:visible />

client:only needs the framework name because Astro cannot detect it without rendering. Since there is no server HTML, use a placeholder to avoid layout shift: <Map client:only="react"><p slot="fallback">Loading map…</p></Map>.

Choosing the latest directive that still feels responsive is the main performance decision in an Astro page. The default, no directive, is the cheapest of all.

What crosses the island boundary

Props for an island are serialized into the HTML so the browser can hydrate with the same values. Strings, numbers, booleans, arrays, plain objects, Date, Map, Set, URL, and similar built-ins are supported. Functions are not, so you cannot pass an onClick handler from an .astro file into a React island; define the handler inside the island instead.

Children work. Content passed between a framework component’s tags is rendered on the server and handed to the island as static HTML:

<Dialog client:idle>
<p>This paragraph is server-rendered Astro content inside a React island.</p>
</Dialog>

Keep island props small. Everything you pass is embedded in the page, and passing a whole content collection to one island undoes the point of shipping less to the browser.

Sharing state between islands

Islands hydrate independently and do not share a React tree, so React context does not connect two islands. Use a small framework-agnostic store instead. Astro’s documentation recommends nanostores:

src/stores/cart.ts
import { atom } from 'nanostores';
export const cartCount = atom(0);
src/components/AddToCart.tsx
import { useStore } from '@nanostores/react';
import { cartCount } from '../stores/cart';
export default function AddToCart() {
const count = useStore(cartCount);
return <button onClick={() => cartCount.set(count + 1)}>Add ({count})</button>;
}

Any island that reads cartCount, in any framework, updates when it changes. Custom DOM events and URL state work too when you want no dependency at all.

Server islands

A server island is the server-side counterpart: a component that renders on the server after the page, so a cached or static page can still contain a per-visitor fragment such as an avatar, a cart total, or a price.

src/pages/product.astro
---
import UserMenu from '../components/UserMenu.astro';
---
<UserMenu server:defer>
<span slot="fallback">Sign in</span>
</UserMenu>

The page is served immediately with the fallback. A small script then requests the island from the server and swaps it in. Inside UserMenu.astro, Astro.cookies and the request are available as in any on-demand route. Server islands need an adapter, since something has to answer that follow-up request (Server rendering in Astro).

Client-side navigation with view transitions

By default every link is a full page load. Adding <ClientRouter /> to a layout’s <head> makes Astro intercept same-site navigation, fetch the next page, and swap it in with an animated transition:

src/layouts/BaseLayout.astro
---
import { ClientRouter } from 'astro:transitions';
---
<html lang="en">
<head>
<ClientRouter />
</head>
<body>
<slot />
</body>
</html>

Name elements with transition:name to morph them between pages, and keep an island alive across navigation, such as a playing audio player, with transition:persist. Scripts that set up the page should listen for the astro:page-load event, because a bundled script runs only once, not after every client-side navigation. Browsers that support cross-document view transitions can also animate plain multi-page navigation with CSS alone, without the router.

Next: Server rendering in Astro.

Check your understanding

Question 1 of 6

0 of 6 checked

  1. 1. What does a framework component do in the browser when used without a directive?

Search articles

Type a word to search published articles.

Open the search page