yaakdoc

Islands

Add interactivity to static pages with islands that hydrate on their own.

Pages are static HTML. The interactive parts are islands: components that are rendered on the server and hydrated individually on the client. Everything else ships no JavaScript.

src/islands.tsx
import { createIsland, useCopy } from "@yaakdoc/react/islands";

export const CopyLink = createIsland("my-theme:copy-link", ({ href }: { href: string }) => {
  const { copied, copy } = useCopy();
  return (
    <button onClick={() => copy({ text: href })}>
      {copied ? "Copied" : "Copy link"}
    </button>
  );
});

Register your islands, along with the built-in ones, in the theme's client entry:

src/client.ts
import { builtinIslands } from "@yaakdoc/react/islands";
import { CopyLink } from "./islands";

export default [...builtinIslands, CopyLink];

Rules

  • Props must be JSON-serializable. They're embedded in the HTML.
  • Islands can't use the data hooks (useDocument, useSite, ...), because they're hydrated in isolation. Pass data in through props.
  • Keep props small. Render big content (code panels, tables) as static HTML, and let the island control it. TabList, for example, only switches between server-rendered panels.

Islands are rendered as isolated roots on the server too, so useId() (and libraries such as Radix UI that rely on it) produce matching ids when they hydrate.

Built-in islands

IslandPurpose
ColorModeToggleSwitches between light and dark mode.
CopyButtonCopies text or the content of an element.
EnvironmentSwitcherSwitches the environment.
TabListControls server-rendered tab panels, and keeps the chosen language in sync across the page.
NavToggleOpens and closes the navigation on small screens.
NavFilterFilters the navigation.
ScrollSpyHighlights the section in view.
OpenInLink"Open in ChatGPT/Claude" links.
RunInYaakLinkThe Run in Yaak button.
SearchTrigger, SearchDialogSearch, in a native <dialog>, opened with Cmd+K / Ctrl+K or /.

Headless hooks

To build your own versions of these, use the hooks: useColorMode, useCopy, useTabs, useNavToggle, useScrollSpy, useEnvironment and useRunInYaakHref, plus openInUrl() and runInYaakUrl() for "Open in ..." and Run in Yaak links.

site.searchUrl is the URL of the search index, or null when search is off. The index is a SearchEntry[] (title, description, href, kind, method, section). Pass the URL to your search island as a prop; the index is fetched when the dialog first opens.

  • useSearchDialog() returns { open, setOpen, restoreFocus }. It opens on openSearch() and the keyboard shortcuts, and restoreFocus() returns focus without scrolling.
  • useSearch(url, { enabled }) returns { query, setQuery, results, active, setActive, onKeyDown, loading, error }. onKeyDown handles ↑, ↓ and Enter.
  • searchEntries(entries, query) is the ranking: every term must match, and titles rank above sections and descriptions.
  • <SearchShortcut /> renders Cmd+K on Apple platforms and Ctrl+K elsewhere. Both are rendered, and a head script shows the right one before first paint, so the label doesn't change when hydrating.
  • useSearchShortcutLabel() returns the same label as a string (for title attributes). It's Cmd+K until hydrated.

On this page