Island
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;
media?: string;
ssrFallback?: VNode;
children?: VNode;
}when— hydration strategy. Defaults to"load". See Hydration strategies below.media— CSS media query string forwhen="media"islands. The island hydrates when this query first matches (including later viewport changes via resize or rotation). Required whenwhen="media"; ignored (with a dev warning) for other strategies.ssrFallback— enables SSR-skip mode (equivalent to Astro'sclient:only). When provided, the heavychildrenare not evaluated server-side;ssrFallbackis 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 four values, all shipped today:
| Value | Behaviour |
|---|---|
"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. |
"media" | Hydrate when a CSS media query first matches (window.matchMedia). Requires the companion media prop. Continues to respond to later viewport changes (resize, orientation). Degrades to immediate hydration when matchMedia is unavailable. Note: SSR-skip (ssrFallback) islands ignore when entirely — they render immediately. |
// Hydrate only on narrow viewports (e.g. mobile menu)
<Island when="media" media="(max-width: 768px)">
<MobileMenu />
</Island>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.
VNode type export
VNode, VNodeArray, and VNodeObject are exported as public types from "@takazudo/zfb":
import type { VNode, VNodeArray, VNodeObject } from "@takazudo/zfb";These structural types are framework-agnostic. VNode includes a bare object member (matching Preact's own ComponentChild design), so Preact's ComponentChildren, VNode<Props>, JSX.Element, and JSX.Element[] are all directly assignable at Island input boundaries — children and ssrFallback — with no as unknown as casts:
import type { ComponentChildren } from "preact";
import { Island } from "@takazudo/zfb";
// Forwarding ComponentChildren as children — no cast needed
function Wrapper({ children }: { children: ComponentChildren }) {
return <Island when="load">{children}</Island>;
}
// Forwarding ComponentChildren as ssrFallback — no cast needed
function SkipWrapper({ fallback }: { fallback: ComponentChildren }) {
return (
<Island ssrFallback={fallback}>
<HeavyWidget />
</Island>
);
}Annotating slots as ComponentChildren (return direction)
When a slot variable holds the return value of <Island>, annotate its type as ComponentChildren or JSX.Element — not as IslandElement. IslandElement is intentionally a narrow structural type that requires props.children, and VNode<{}> from Preact is NOT assignable to it by design. Use a Preact-native type for the slot instead:
import type { ComponentChildren } from "preact";
function Page() {
const slot: ComponentChildren = (
<Island when="visible">
<Counter />
</Island>
);
return <main>{slot}</main>;
}Name-collision note for Preact consumers
If a file already has import { VNode } from "preact", use a qualified import to avoid the name clash:
import type { VNode as ZfbVNode } from "@takazudo/zfb";Exported constants and helpers
The following are exported from "@takazudo/zfb" alongside Island:
HYDRATE_MARKER_ATTR— thedata-zfb-islandattribute name.SKIP_SSR_MARKER_ATTR— thedata-zfb-island-skip-ssrattribute name.ANONYMOUS_COMPONENT_NAME— fallback name used when a wrapped child's identity cannot be determined.resolveWhen(when: unknown): When— validates and normalises awhenvalue. 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.
Footgun: do not self-wrap an island
Nested island markers mis-hydrate
Do not place an <Island> wrapper inside a component's own render output. The runtime discovers all island markers in the DOM and mounts each one — if a marker is nested inside another, two independent framework instances try to own the same DOM subtree, causing conflicts.
Wrong — <Island> inside the component itself:
/ / Counter. tsx — do NOT do this export default function Counter() { return ( <Island when= "idle"> <InnerWidget / > </ Island> ); }Right — apply <Island> at the call site:
/ / Page. tsx <Island when= "idle"> <Counter / > </ Island>In development the runtime emits a console.warn naming the offending component when it detects this pattern.