Renders the island wrapped in a hydration container. The output includes everything ilha.mount() needs to activate the island on the client — the rendered HTML, serialized props, and optionally a state snapshot — all embedded as data attributes on a wrapper element.
Use this method in your SSR handler whenever you want the island to become interactive in the browser without a full client-side re-render.
Basic usage
import const ilha: RootBuilder & DirectIslandFactory & {
html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
raw: (value: string) => RawHtml;
mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
const const MyIsland: Island<RootInput, MergeState<RootState, "count", number>>MyIsland = const ilha: RootBuilder & DirectIslandFactory & {
html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
raw: (value: string) => RawHtml;
mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<number, "count">(key: "count", init?: StateInit<RootInput, number> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>state("count", 0)
.IlhaBuilder<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "count", number>>render(({ state: IslandState<MergeState<RootState, "count", number>>state }) => <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{state: IslandState<MergeState<RootState, "count", number>>state.count: MarkedSignalAccessor
() => number (+1 overload)
count()}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>);
const const html: stringhtml = await const MyIsland: Island<RootInput, MergeState<RootState, "count", number>>MyIsland
.Island<RootInput, MergeState<RootState, "count", number>>.hydratable(props: Partial<RootInput>, options: HydratableOptions): Promise<string>hydratable({ count: numbercount: 42 }, { HydratableOptions.name: stringname: "MyIsland" });
// → '<div data-ilha="MyIsland" data-ilha-props="{"count":42}">
// <p>42</p>
// </div>'Options
interface HydratableOptions {
name: string; // required
as?: string; // default: "div"
snapshot?: boolean | { state?: boolean; derived?: boolean }; // default: false
skipOnMount?: boolean; // default: false
}| Option | Type | Default | Description |
|---|---|---|---|
name |
string |
— | Registry key used by mount() to find the matching island on the client |
as |
string |
"div" |
Tag name for the wrapper element |
snapshot |
boolean | object |
false |
Embed state and/or derived values in data-ilha-state |
skipOnMount |
boolean |
false |
Skip all .onMount() callbacks when hydrating from snapshot |
The name option
The name must match the key used when registering the island in your client-side mount() or hydrate() call:
// server
import const ilha: RootBuilder & DirectIslandFactory & {
html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
raw: (value: string) => RawHtml;
mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
import { const mount: (registry: IslandRegistry, options?: MountOptions) => MountResultmount } from "ilha";
const const Counter: Island<RootInput, MergeState<RootState, "count", number>>Counter = const ilha: RootBuilder & DirectIslandFactory & {
html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
raw: (value: string) => RawHtml;
mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<number, "count">(key: "count", init?: StateInit<RootInput, number> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>state("count", 0)
.IlhaBuilder<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "count", number>>render(({ state: IslandState<MergeState<RootState, "count", number>>state }) => <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{state: IslandState<MergeState<RootState, "count", number>>state.count: MarkedSignalAccessor
() => number (+1 overload)
count()}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>);
const const html: stringhtml = await const Counter: Island<RootInput, MergeState<RootState, "count", number>>Counter.Island<RootInput, MergeState<RootState, "count", number>>.hydratable(props: Partial<RootInput>, options: HydratableOptions): Promise<string>hydratable({}, { HydratableOptions.name: stringname: "Counter" });
// client
function mount(registry: IslandRegistry, options?: MountOptions): MountResultmount({ type Counter: Island<RootInput, MergeState<RootState, "count", number>>Counter }); // ← "Counter" matches the name aboveIf the name has no match in the registry, mount() skips the element silently.
The snapshot option
Snapshots embed current signal values into data-ilha-state so the client can restore them on mount without re-computing or re-fetching.
import const ilha: RootBuilder & DirectIslandFactory & {
html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
raw: (value: string) => RawHtml;
mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
const const Counter: Island<RootInput, MergeState<RootState, "count", number>>Counter = const ilha: RootBuilder & DirectIslandFactory & {
html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
raw: (value: string) => RawHtml;
mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<number, "count">(key: "count", init?: StateInit<RootInput, number> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>state("count", 0)
.IlhaBuilder<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "count", number>>render(({ state: IslandState<MergeState<RootState, "count", number>>state }) => <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{state: IslandState<MergeState<RootState, "count", number>>state.count: MarkedSignalAccessor
() => number (+1 overload)
count()}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>);
// Snapshot state only
await const Counter: Island<RootInput, MergeState<RootState, "count", number>>Counter.Island<RootInput, MergeState<RootState, "count", number>>.hydratable(props: Partial<RootInput>, options: HydratableOptions): Promise<string>hydratable(
{ count: numbercount: 5 },
{ HydratableOptions.name: stringname: "Counter", HydratableOptions.snapshot?: boolean | {
state?: boolean;
derived?: boolean;
} | undefined
snapshot: true },
);
// → data-ilha-state='{"count":5}'
// Fine-grained control
await const Counter: Island<RootInput, MergeState<RootState, "count", number>>Counter.Island<RootInput, MergeState<RootState, "count", number>>.hydratable(props: Partial<RootInput>, options: HydratableOptions): Promise<string>hydratable(
{ count: numbercount: 5 },
{
HydratableOptions.name: stringname: "Counter",
HydratableOptions.snapshot?: boolean | {
state?: boolean;
derived?: boolean;
} | undefined
snapshot: { state?: boolean | undefinedstate: true, derived?: boolean | undefinedderived: false },
},
);snapshot value |
State snapshotted | Derived snapshotted |
|---|---|---|
false |
No | No |
true |
Yes | Yes |
{ state: true, derived: false } |
Yes | No |
{ state: false, derived: true } |
No | Yes |
When no snapshot is set, the island mounts fresh on the client — state initializers run again and .onMount() always fires.
The skipOnMount option
When restoring from a snapshot, you often do not want .onMount() to run — the DOM is already correct and setup work would be redundant. Set skipOnMount: true to suppress all .onMount() callbacks during hydration:
import const ilha: RootBuilder & DirectIslandFactory & {
html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
raw: (value: string) => RawHtml;
mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
const const Island: Island<RootInput, RootState>Island = const ilha: RootBuilder & DirectIslandFactory & {
html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
raw: (value: string) => RawHtml;
mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.onMount(fn: (ctx: OnMountContext<RootInput, RootState, RootDerived, RootActions>) => (() => void) | void): IlhaBuilder<RootInput, RootState, RootDerived, RootActions>onMount(() => {
var console: Consoleconsole.Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log("this is skipped on hydration");
})
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, RootState, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, RootState>render(() => <"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>hello</"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>);
await const Island: Island<RootInput, RootState>Island.Island<RootInput, RootState>.hydratable(props: Partial<RootInput>, options: HydratableOptions): Promise<string>hydratable(
{},
{
HydratableOptions.name: stringname: "my-island",
HydratableOptions.snapshot?: boolean | {
state?: boolean;
derived?: boolean;
} | undefined
snapshot: true,
HydratableOptions.skipOnMount?: boolean | undefinedskipOnMount: true,
},
);Note that skipOnMount only suppresses .onMount() — .effect() callbacks always run on mount regardless.
The as option
The wrapper element tag defaults to "div". Change it when the surrounding HTML requires a specific element — for example inside a <ul> where a <div> would be invalid:
import const ilha: RootBuilder & DirectIslandFactory & {
html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
raw: (value: string) => RawHtml;
mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
const const Item: Island<RootInput, RootState>Item = ilha<RootInput>(fn: (ctx: RenderContext<RootInput, RootState, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, RootState>ilha(() => <"li": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLLIElement> & {
value?: number;
}>
li>item</"li": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLLIElement> & {
value?: number;
}>
li>);
await const Item: Island<RootInput, RootState>Item.Island<RootInput, RootState>.hydratable(props: Partial<RootInput>, options: HydratableOptions): Promise<string>hydratable({}, { HydratableOptions.name: stringname: "item", HydratableOptions.as?: string | undefinedas: "li" });
// → '<li data-ilha="item">…</li>'SSR output structure
The full rendered output looks like this:
<div
data-ilha="MyIsland"
data-ilha-props='{"count":42}'
data-ilha-state='{"count":42}'
>
<p>42</p>
</div>data-ilha— the registry key, used bymount()for discovery.data-ilha-props— serialized input props, read automatically onmount().data-ilha-state— serialized signal snapshot, only present whensnapshotis set.
With scoped styles
If the island uses .css(), the <style> tag is included inside the wrapper regardless of the snapshot option:
<div data-ilha="Card">
<style data-ilha-css>
@scope (:scope) to ([data-ilha]) {
.title {
font-weight: 700;
}
}
</style>
<div>
<p class="title">Hello</p>
</div>
</div>With @ilha/router
When using file-system routing, .hydratable() is called internally by renderHydratable() and renderResponse(). You typically do not call it directly — the router handles it:
import { pageRouter, registry } from "ilha:pages/server";
// The router calls .hydratable() internally for the matched island
const html = await pageRouter.renderHydratable(
request.url,
registry,
);On the client, import from ilha:pages/client (see Router — virtual modules).
For manual setups without the router, call .hydratable() directly in your SSR handler.
Full SSR + hydration example
// server.ts
import const ilha: RootBuilder & DirectIslandFactory & {
html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
raw: (value: string) => RawHtml;
mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha, { const mount: (registry: IslandRegistry, options?: MountOptions) => MountResultmount } from "ilha";
const const Counter: Island<RootInput, MergeState<RootState, "count", number>>Counter = const ilha: RootBuilder & DirectIslandFactory & {
html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
raw: (value: string) => RawHtml;
mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<number, "count">(key: "count", init?: StateInit<RootInput, number> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>state("count", 0)
.IlhaBuilder<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions>.action<"increment", undefined, void>(key: "increment", fn: (props: undefined, ctx: ActionContext<RootInput, MergeState<RootState, "count", number>, RootDerived>) => void): IlhaBuilder<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions & Record<"increment", (props: undefined, ctx: ActionContext<RootInput, MergeState<RootState, "count", number>, RootDerived>) => void>>action("increment", (_: undefined_, { state: IslandState<MergeState<RootState, "count", number>>state }) => {
state: IslandState<MergeState<RootState, "count", number>>state.count: MarkedSignalAccessor
(value: SignalSetter<number>) => void (+1 overload)
count((count: numbercount) => count: numbercount + 1);
})
.IlhaBuilder<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions & Record<"increment", (props: undefined, ctx: ActionContext<...>) => void>>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "count", number>, RootDerived, RootActions & Record<"increment", (props: undefined, ctx: ActionContext<RootInput, MergeState<RootState, "count", number>, RootDerived>) => void>>) => string | RawHtml): Island<RootInput, MergeState<RootState, "count", number>>render(({ state: IslandState<MergeState<RootState, "count", number>>state, action: IslandActions<RootActions & Record<"increment", (props: undefined, ctx: ActionContext<RootInput, MergeState<RootState, "count", number>, RootDerived>) => void>>action }) => (
<"button": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLButtonElement> & ButtonAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
button onclick?: NativeEventHandler<PointerEvent & {
readonly currentTarget: HTMLButtonElement;
}> | undefined
onclick={action: IslandActions<RootActions & Record<"increment", (props: undefined, ctx: ActionContext<RootInput, MergeState<RootState, "count", number>, RootDerived>) => void>>action.increment: ActionAccessor<undefined, void>increment}>
Count: {state: IslandState<MergeState<RootState, "count", number>>state.count: MarkedSignalAccessor
() => number (+1 overload)
count()}
</"button": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLButtonElement> & ButtonAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
button>
));
// Server — render with snapshot
const const body: stringbody = await const Counter: Island<RootInput, MergeState<RootState, "count", number>>Counter.Island<RootInput, MergeState<RootState, "count", number>>.hydratable(props: Partial<RootInput>, options: HydratableOptions): Promise<string>hydratable(
{ count: numbercount: 10 },
{ HydratableOptions.name: stringname: "Counter", HydratableOptions.snapshot?: boolean | {
state?: boolean;
derived?: boolean;
} | undefined
snapshot: true, HydratableOptions.skipOnMount?: boolean | undefinedskipOnMount: true },
);
// Client — hydrate in place
function mount(registry: IslandRegistry, options?: MountOptions): MountResultmount({ type Counter: Island<RootInput, MergeState<RootState, "count", number>>Counter });Dev warnings for non-JSON-safe snapshots
In development, if a snapshotted state or derived value contains something that does not round-trip cleanly through JSON — undefined, functions, bigint, non-finite numbers (NaN, Infinity), Date instances, and similar — ilha logs a warning identifying the offending path and reason:
import const ilha: RootBuilder & DirectIslandFactory & {
html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
raw: (value: string) => RawHtml;
mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
const const Island: Island<RootInput, MergeState<RootState, "when", Date>>Island = const ilha: RootBuilder & DirectIslandFactory & {
html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
raw: (value: string) => RawHtml;
mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<Date, "when">(key: "when", init?: StateInit<RootInput, Date> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "when", Date>, RootDerived, RootActions>state("when", () => new var Date: DateConstructor
new () => Date (+4 overloads)
Date())
.IlhaBuilder<RootInput, MergeState<RootState, "when", Date>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "when", Date>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "when", Date>>render(() => <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p></"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>);
await const Island: Island<RootInput, MergeState<RootState, "when", Date>>Island.Island<RootInput, MergeState<RootState, "when", Date>>.hydratable(props: Partial<RootInput>, options: HydratableOptions): Promise<string>hydratable({}, { HydratableOptions.name: stringname: "lossy", HydratableOptions.snapshot?: boolean | {
state?: boolean;
derived?: boolean;
} | undefined
snapshot: true });
// → warns: state.when is not JSON-safe (Date)The snapshot still serializes — the warning is a heads-up, not a thrown error — but the restored value on the client will differ from what you set on the server (a Date becomes an ISO string, undefined fields are dropped, etc.). Keep snapshotted state and derived values plain and JSON-safe to avoid surprises on hydration. The warning is suppressed in production.
Notes
.hydratable()is always async — it awaits all.derived()values before rendering, regardless of whether the snapshot includes them.- Props are JSON-serialized into
data-ilha-props. Values that are not JSON-serializable (functions, class instances, circular references) will cause a runtime error. Keep props plain and serializable. - The snapshot serializes signal values at the moment
.hydratable()is called. If state changes after this point on the server, those changes are not reflected in the snapshot.