Islands
An island is a component that hydrates on the client. Make one by putting it under app/islands/ (reserved at any depth). There is no island() wrapper. Its default export hydrates.
// app/islands/counter.tsx (interactive on the client after hydration)
import { useState } from "preact/hooks";
export default function Counter({ start = 0 }: { start?: number }) {
const [n, setN] = useState(start);
return (
<button type="button" onClick={() => setN((v) => v + 1)}>
counts: {n}
</button>
);
}Import it into a page like any component and pass props. Chevalier serializes the props and hydrates the island in the browser. A page that renders no islands emits no client script at all.
What a page ships
An island is server-rendered to HTML like any component, then hydrated in the browser — the markup arrives complete, and the JavaScript only takes over the interactive part. A page that imports one island ships exactly that island's worth of JavaScript, and nothing else.
That is the whole model: adding an island never grows a page that doesn't use it. This documentation site is the demonstration — it renders no islands, so it ships zero client JavaScript on every page, and a test asserts that against the production build.
Hot reload
In dev, islands hot-update in place with their state preserved (Preact Fast Refresh). Edit counter.tsx and the count stays where it was. Edit a route or a _layout.tsx and the page does a full reload instead — there's no island state to preserve, so a reload is simpler and always correct.