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.