zfb

Type to search...

to open search from anywhere

Island

CreatedJun 1, 2026Takeshi Takatsudo

Wrap a component as a client-hydrated island using the Island JSX wrapper.

The <Island> wrapper

Import Island from "@takazudo/zfb" and wrap any component that should hydrate in the browser. Everything else stays server-rendered HTML.

import { Island } from "@takazudo/zfb";
import Counter from "./Counter";

export default function Page() {
  return (
    <main>
      <h1>My Page</h1>
      <Island when="visible">
        <Counter />
      </Island>
    </main>
  );
}

At SSR time the <Island> wrapper emits a <div data-zfb-island="Counter" data-when="visible"> marker with the server-rendered child inside. The client runtime (@takazudo/zfb-runtime) queries these markers and mounts the component when the when condition fires.

IslandProps

interface IslandProps {
  when?: When;
  ssrFallback?: VNode;
  children?: VNode;
}
  • when — hydration strategy. Defaults to "load". See Hydration strategies below.
  • ssrFallback — enables SSR-skip mode (equivalent to Astro’s client:only). When provided, the heavy children are not evaluated server-side; ssrFallback is rendered in their place and the client swaps in the real component on hydration.
  • children — the component to hydrate.

Hydration strategies

The when prop accepts three values, all shipped today:

ValueBehaviour
"load"Hydrate immediately when the page’s JavaScript runs. This is the default when when is omitted.
"visible"Hydrate when the island’s root element enters the viewport (IntersectionObserver, threshold 0). Cheapest deferral for off-screen content.
"idle"Hydrate during the browser’s next idle callback. Falls back to setTimeout(0) on platforms without requestIdleCallback.

SSR-skip mode

Pass ssrFallback to skip server rendering the heavy child entirely:

import { Island } from "@takazudo/zfb";
import HeavyChart from "./HeavyChart";

export default function Page() {
  return (
    <Island ssrFallback={<div>Loading chart…</div>}>
      <HeavyChart data={data} />
    </Island>
  );
}

The server emits <div data-zfb-island-skip-ssr="HeavyChart" data-when="load">…fallback…</div>. On hydration the client runtime renders HeavyChart into the placeholder.

Exported constants and helpers

The following are exported from "@takazudo/zfb" alongside Island:

  • HYDRATE_MARKER_ATTR — the data-zfb-island attribute name.
  • SKIP_SSR_MARKER_ATTR — the data-zfb-island-skip-ssr attribute name.
  • ANONYMOUS_COMPONENT_NAME — fallback name used when a wrapped child’s identity cannot be determined.
  • resolveWhen(when: unknown): When — validates and normalises a when value. Unknown inputs fall back to "load" (with a warning in development).

“use client” directive

zfb also supports a "use client" file-level directive as an alternative authoring style. A component file whose first statement is the literal string "use client" is treated as an island entry by the build scanner (crates/zfb-islands). The esbuild-backed bundler compiles it to ESM separately, with its own dependency graph.

"use client";

import { useState } from "preact/hooks";

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

Import the island from a server component or page module. The build pipeline wires the hydration bootstrap automatically.

For the broader story on islands architecture and when to reach for one, see Islands.

Revision History