Input

Declares the island's external props and their types. Two forms are supported: a type-only generic for when you just need TypeScript inference, and a Standard Schema-compatible validator (Zod, Valibot, ArkType, etc.) for runtime validation too.

Basic usage

Type-only — TypeScript inference, no runtime validation:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
from "ilha";
const
const Greeting: Island<{
    name: string;
}, Record<string, never>>
Greeting
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.input<{
    name: string;
}>(): IlhaBuilder<{
    name: string;
}, Record<string, never>, Record<string, never>> (+1 overload)
input
<{
name: string
name
: string }>()
.
IlhaBuilder<{ name: string; }, Record<string, never>, Record<string, never>>.render(fn: (ctx: RenderContext<{
    name: string;
}, Record<string, never>, Record<string, never>>) => string | RawHtml): Island<{
    name: string;
}, Record<string, never>>
render
(({
input: {
    name: string;
}
input
}) => `<p>Hello, ${
input: {
    name: string;
}
input
.
name: string
name
}!</p>`);
const Greeting: Island<{
    name: string;
}, Record<string, never>>
Greeting
.
Island<{ name: string; }, Record<string, never>>.toString(props?: Partial<{
    name: string;
}> | undefined): string
toString
({
name?: string | undefined
name
: "ilha" }); // → <p>Hello, ilha!</p>

With a schema — inference plus runtime validation and coercion:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
from "ilha";
import {
import z
z
} from "zod";
const
const Greeting: Island<{
    name: string;
} & Record<string, unknown>, Record<string, never>>
Greeting
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.input<z.ZodObject<{
    name: z.ZodDefault<z.ZodString>;
}, z.core.$strip>>(schema: z.ZodObject<{
    name: z.ZodDefault<z.ZodString>;
}, z.core.$strip>): IlhaBuilder<{
    name: string;
} & Record<string, unknown>, Record<string, never>, Record<string, never>> (+1 overload)
input
(
import z
z
.
function object<{
    name: z.ZodDefault<z.ZodString>;
}>(shape?: {
    name: z.ZodDefault<z.ZodString>;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    name: z.ZodDefault<z.ZodString>;
}, z.core.$strip>
object
({
name: z.ZodDefault<z.ZodString>
name
:
import z
z
.
function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)
string
().
ZodType<any, any, $ZodStringInternals<string>>.default(def: string): z.ZodDefault<z.ZodString> (+1 overload)
default
("World") }))
.
IlhaBuilder<{ name: string; } & Record<string, unknown>, Record<string, never>, Record<string, never>>.render(fn: (ctx: RenderContext<{
    name: string;
} & Record<string, unknown>, Record<string, never>, Record<string, never>>) => string | RawHtml): Island<{
    name: string;
} & Record<string, unknown>, Record<string, never>>
render
(({
input: {
    name: string;
} & Record<string, unknown>
input
}) => `<p>Hello, ${
input: {
    name: string;
} & Record<string, unknown>
input
.
name: string
name
}!</p>`);
const Greeting: Island<{
    name: string;
} & Record<string, unknown>, Record<string, never>>
Greeting
.
Island<{ name: string; } & Record<string, unknown>, Record<string, never>>.toString(props?: Partial<{
    name: string;
} & Record<string, unknown>> | undefined): string
toString
({
name?: string | undefined
name
: "ilha" }); // → <p>Hello, ilha!</p>
const Greeting: Island<{
    name: string;
} & Record<string, unknown>, Record<string, never>>
Greeting
.
Island<{ name: string; } & Record<string, unknown>, Record<string, never>>.toString(props?: Partial<{
    name: string;
} & Record<string, unknown>> | undefined): string
toString
(); // → <p>Hello, World!</p>

Why use .input()

Without .input(), any props passed to an island are untyped and unvalidated. Adding a type or schema gives you:

  • Full TypeScript inference for input inside .state(), .render(), .on(), .effect(), and every other builder method.
  • Runtime validation and coercion on every call, including during SSR and hydration (schema form only).
  • Default values handled by the schema itself, so the island works without props (schema form only).

Choosing a form

.input<T>().input(schema)
TypeScript inference
Runtime validation
Default values✓ (via schema)
Extra dependency✓ (Zod, Valibot, etc.)

Use .input<T>() for simple islands where you control the call sites and don't need validation. Use .input(schema) when you need defaults, coercion, or validation — especially for islands that are hydrated from serialized server props.

Using defaults

Defaults are defined in the schema, not in ilha. Any Standard Schema validator that supports defaults will apply them automatically when a prop is omitted.

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
from "ilha";
import {
import z
z
} from "zod";
const
const Card: Island<{
    title: string;
    accent: string;
} & Record<string, unknown>, Record<string, never>>
Card
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.input<z.ZodObject<{
    title: z.ZodString;
    accent: z.ZodDefault<z.ZodString>;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
    accent: z.ZodDefault<z.ZodString>;
}, z.core.$strip>): IlhaBuilder<{
    title: string;
    accent: string;
} & Record<string, unknown>, Record<string, never>, Record<string, never>> (+1 overload)
input
(
import z
z
.
function object<{
    title: z.ZodString;
    accent: z.ZodDefault<z.ZodString>;
}>(shape?: {
    title: z.ZodString;
    accent: z.ZodDefault<z.ZodString>;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
    accent: z.ZodDefault<z.ZodString>;
}, z.core.$strip>
object
({
title: z.ZodString
title
:
import z
z
.
function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)
string
(),
accent: z.ZodDefault<z.ZodString>
accent
:
import z
z
.
function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)
string
().
ZodType<any, any, $ZodStringInternals<string>>.default(def: string): z.ZodDefault<z.ZodString> (+1 overload)
default
("teal"),
}), ) .
IlhaBuilder<{ title: string; accent: string; } & Record<string, unknown>, Record<string, never>, Record<string, never>>.render(fn: (ctx: RenderContext<{
    title: string;
    accent: string;
} & Record<string, unknown>, Record<string, never>, Record<string, never>>) => string | RawHtml): Island<{
    title: string;
    accent: string;
} & Record<string, unknown>, Record<string, never>>
render
(({
input: {
    title: string;
    accent: string;
} & Record<string, unknown>
input
}) => `<div style="color:${
input: {
    title: string;
    accent: string;
} & Record<string, unknown>
input
.
accent: string
accent
}">${
input: {
    title: string;
    accent: string;
} & Record<string, unknown>
input
.
title: string
title
}</div>`);

State initialized from input

Once you have typed input, you can use it to initialize state:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
from "ilha";
import {
import z
z
} from "zod";
const
const Counter: Island<{
    start: number;
} & Record<string, unknown>, MergeState<Record<string, never>, "count", number>>
Counter
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.input<z.ZodObject<{
    start: z.ZodDefault<z.ZodNumber>;
}, z.core.$strip>>(schema: z.ZodObject<{
    start: z.ZodDefault<z.ZodNumber>;
}, z.core.$strip>): IlhaBuilder<{
    start: number;
} & Record<string, unknown>, Record<string, never>, Record<string, never>> (+1 overload)
input
(
import z
z
.
function object<{
    start: z.ZodDefault<z.ZodNumber>;
}>(shape?: {
    start: z.ZodDefault<z.ZodNumber>;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    start: z.ZodDefault<z.ZodNumber>;
}, z.core.$strip>
object
({
start: z.ZodDefault<z.ZodNumber>
start
:
import z
z
.
function number(params?: string | z.core.$ZodNumberParams): z.ZodNumber
number
().
ZodType<any, any, $ZodNumberInternals<number>>.default(def: number): z.ZodDefault<z.ZodNumber> (+1 overload)
default
(0) }))
.
IlhaBuilder<{ start: number; } & Record<string, unknown>, Record<string, never>, Record<string, never>>.state<number, "count">(key: "count", init?: StateInit<{
    start: number;
} & Record<string, unknown>, number> | undefined): IlhaBuilder<{
    start: number;
} & Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>
state
("count", ({
start: number
start
}) =>
start: number
start
)
.
IlhaBuilder<{ start: number; } & Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>.render(fn: (ctx: RenderContext<{
    start: number;
} & Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>) => string | RawHtml): Island<{
    start: number;
} & Record<string, unknown>, MergeState<Record<string, never>, "count", number>>
render
(({
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
}) => `<p>${
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
.
count: MarkedSignalAccessor
() => number (+1 overload)
count
()}</p>`);

The initializer function receives the resolved input object, so state stays in sync with whatever props were passed in. This works identically with both forms.

Async schemas

Async schemas are not supported. If your validator's validate() method returns a Promise, ilha will throw at runtime. Keep schemas synchronous.

Notes

  • .input() must be called before any other builder method if you want the input type to flow through the chain.
  • Calling .input() resets the builder — any previously chained .state() or other methods are not carried over.
  • If .input() is omitted entirely, props are accepted as Record<string, unknown> with no validation.