Hydratable

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: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha"; const const MyIsland: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>MyIsland =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>state("count", 0) .IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>render(({ state: IslandState<MergeState<Record<never, never>, "count", number>>state }) => <IntrinsicElements[string]: anyp>{state: IslandState<MergeState<Record<never, never>, "count", number>>state.
count: MarkedSignalAccessor
() => number (+1 overload)
count
()}IntrinsicElements[string]: anyp>); const const html: stringhtml = await const MyIsland: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>MyIsland .Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string>hydratable({ count: numbercount: 42 }, { HydratableOptions.name: stringname: "MyIsland" }); // → ' //

42

// '

Options

interface HydratableOptions {
  name: string; // required
  as?: string; // default: "div"
  snapshot?: boolean | { state?: boolean; derived?: boolean }; // default: false
  skipOnMount?: boolean; // default: false
}
OptionTypeDefaultDescription
namestringRegistry key used by mount() to find the matching island on the client
asstring"div"Tag name for the wrapper element
snapshotboolean | objectfalseEmbed state and/or derived values in data-ilha-state
skipOnMountbooleanfalseSkip 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: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha"; import { const mount: (registry: IslandRegistry, options?: MountOptions) => MountResultmount } from "ilha"; const const Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>state("count", 0) .IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>render(({ state: IslandState<MergeState<Record<never, never>, "count", number>>state }) => <IntrinsicElements[string]: anyp>{state: IslandState<MergeState<Record<never, never>, "count", number>>state.
count: MarkedSignalAccessor
() => number (+1 overload)
count
()}IntrinsicElements[string]: anyp>); const const html: stringhtml = await const Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter.Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string>hydratable({}, { HydratableOptions.name: stringname: "Counter" }); // client function mount(registry: IslandRegistry, options?: MountOptions): MountResultmount({ type Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter }); // ← "Counter" matches the name above

If 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: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha"; const const Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>state("count", 0) .IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>render(({ state: IslandState<MergeState<Record<never, never>, "count", number>>state }) => <IntrinsicElements[string]: anyp>{state: IslandState<MergeState<Record<never, never>, "count", number>>state.
count: MarkedSignalAccessor
() => number (+1 overload)
count
()}IntrinsicElements[string]: anyp>); // Snapshot state only await const Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter.Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, 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<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter.Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, 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 valueState snapshottedDerived snapshotted
falseNoNo
trueYesYes
{ state: true, derived: false }YesNo
{ state: false, derived: true }NoYes

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: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha"; const const Island: Island<Record<string, unknown>, Record<never, never>>Island =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.onMount(fn: (ctx: OnMountContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => (() => void) | void): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>onMount(() => { var console: Consoleconsole.Console.log(...data: any[]): void
The **`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<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => <IntrinsicElements[string]: anydiv>helloIntrinsicElements[string]: anydiv>); await const Island: Island<Record<string, unknown>, Record<never, never>>Island.Island<Record<string, unknown>, Record<never, never>>.hydratable(props: Partial<Record<string, unknown>>, 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 where a
would be invalid:
import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha"; const const Item: Island<Record<string, unknown>, Record<never, never>>Item =
const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
    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;
    ... 4 more ...;
    onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<never, never>>render(() => <IntrinsicElements[string]: anyli>itemIntrinsicElements[string]: anyli>); await const Item: Island<Record<string, unknown>, Record<never, never>>Item.Island<Record<string, unknown>, Record<never, never>>.hydratable(props: Partial<Record<string, unknown>>, options: HydratableOptions): Promise<string>hydratable({}, { HydratableOptions.name: stringname: "item", HydratableOptions.as?: string | undefinedas: "li" }); // → '
  • '

    SSR output structure

    The full rendered output looks like this:

    42

    • data-ilha — the registry key, used by mount() for discovery.
    • data-ilha-props — serialized input props, read automatically on mount().
    • data-ilha-state — serialized signal snapshot, only present when snapshot is set.

    With scoped styles

    If the island uses .css(), the

    Hello

    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: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
        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;
        ... 4 more ...;
        onUncaughtError: typeof onUncaughtError;
    }
    ilha, { const mount: (registry: IslandRegistry, options?: MountOptions) => MountResultmount } from "ilha"; const const Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter =
    const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
        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;
        ... 4 more ...;
        onUncaughtError: typeof onUncaughtError;
    }
    ilha
    .IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>state("count", 0) .IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>.on<"button@click">(selectorOrCombined: "button@click", handler: (ctx: HandlerContextFor<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, "click", Record<never, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>on("button@click", ({ state: IslandState<MergeState<Record<never, never>, "count", number>>state }) => state: IslandState<MergeState<Record<never, never>, "count", number>>state.
    count: MarkedSignalAccessor
    (value: number) => void (+1 overload)
    count
    (state: IslandState<MergeState<Record<never, never>, "count", number>>state.
    count: MarkedSignalAccessor
    () => number (+1 overload)
    count
    () + 1),
    ) .IlhaBuilder<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<never, never>, "count", number>, Record<never, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>render(({ state: IslandState<MergeState<Record<never, never>, "count", number>>state }) => ( <IntrinsicElements[string]: anydiv> <IntrinsicElements[string]: anyp>Count: {state: IslandState<MergeState<Record<never, never>, "count", number>>state.
    count: MarkedSignalAccessor
    () => number (+1 overload)
    count
    ()}IntrinsicElements[string]: anyp> <IntrinsicElements[string]: anybutton>IncrementIntrinsicElements[string]: anybutton> IntrinsicElements[string]: anydiv> )); // Server — render with snapshot const const body: stringbody = await const Counter: Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter.Island<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>.hydratable(props: Partial<Record<string, unknown>>, 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<Record<string, unknown>, MergeState<Record<never, never>, "count", number>>Counter });

    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.