Template bindings
Two-way bindings between form elements and signals, including nested-field select, component contracts, and radio and checkbox groups.
Inside JSX, use bind:property={signal} to create two-way bindings between form elements and signals. When the signal changes, the element updates. When the user interacts with the element, the signal updates.
Use bind:* for synchronization and lowercase events for custom logic. You can put both on the same element, such as <input bind:value={state.name} onchange={validate} />.
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;
... 6 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha } from "ilha";
const const Name: Island<RootInput, MergeState<RootState, "name", string>>Name = 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;
... 6 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<string, "name">(key: "name", init?: StateInit<RootInput, string> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "name", string>, RootDerived, RootActions>state("name", "Ada").IlhaBuilder<RootInput, MergeState<RootState, "name", string>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "name", string>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "name", string>>render(({ state: IslandState<MergeState<RootState, "name", string>>state }) => (
<"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>
<"input": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLInputElement> & InputAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
input "bind:value"?: BindAccessor<string | number> | undefinedbind:"bind:value"?: BindAccessor<string | number> | undefinedvalue={state: IslandState<MergeState<RootState, "name", string>>state.name: SignalAccessor<string>name} />
<"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>Hello, {state: IslandState<MergeState<RootState, "name", string>>state.name: MarkedSignalAccessor
() => string (+1 overload)
name()}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>
</"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>
));
Supported bindings
| Binding | Element | Bound property | Trigger event |
|---|---|---|---|
bind:value |
<input>, <textarea>, <select> |
value |
input |
bind:valueAsNumber |
<input type="number"> |
valueAsNumber |
input |
bind:valueAsDate |
<input type="date"> |
valueAsDate |
input |
bind:checked |
<input type="checkbox"> |
checked |
change |
bind:group |
<input type="radio">, <input type="checkbox"> |
checked / value |
change |
bind:open |
<details> |
open |
toggle |
bind:files |
<input type="file"> |
files |
change |
bind:this |
Any element | element reference | — |
The element type is detected at runtime — no configuration needed.
Number coercion
bind:value and radio-group bind:group coerce the DOM string value back to a number when the bound signal currently holds a number. If the value cannot be parsed (empty or non-numeric), ilha coerces to 0 and logs a dev warning. Prefer bind:valueAsNumber for numeric inputs — it yields null on invalid input rather than silently coercing to 0:
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;
... 6 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha } from "ilha";
const const Age: Island<RootInput, MergeState<RootState, "age", number>>Age = 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;
... 6 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<number, "age">(key: "age", init?: StateInit<RootInput, number> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "age", number>, RootDerived, RootActions>state("age", 0)
.IlhaBuilder<RootInput, MergeState<RootState, "age", number>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "age", number>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "age", number>>render(({ state: IslandState<MergeState<RootState, "age", number>>state }) => (
<"input": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLInputElement> & InputAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
input type?: "number" | RawHtml | "button" | "search" | "time" | "image" | "text" | "reset" | "submit" | "hidden" | "checkbox" | "color" | "date" | "datetime-local" | "email" | "file" | "month" | "password" | "radio" | "range" | "tel" | "url" | "week" | undefinedtype="number" "bind:valueAsNumber"?: BindAccessor<number | null> | undefinedbind:"bind:valueAsNumber"?: BindAccessor<number | null> | undefinedvalueAsNumber={state: IslandState<MergeState<RootState, "age", number>>state.age: SignalAccessor<number>age} />
));
Checkbox-array bind:group is exempt: an option value that fails to parse is kept as its raw string.
Radio and checkbox groups
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;
... 6 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha } from "ilha";
const const Plan: Island<RootInput, MergeState<RootState, "plan", string>>Plan = 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;
... 6 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<string, "plan">(key: "plan", init?: StateInit<RootInput, string> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "plan", string>, RootDerived, RootActions>state("plan", "pro").IlhaBuilder<RootInput, MergeState<RootState, "plan", string>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "plan", string>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "plan", string>>render(({ state: IslandState<MergeState<RootState, "plan", string>>state }) => (
<>
<"input": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLInputElement> & InputAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
input
type?: "number" | RawHtml | "button" | "search" | "time" | "image" | "text" | "reset" | "submit" | "hidden" | "radio" | "checkbox" | "color" | "date" | "datetime-local" | "email" | "file" | "month" | "password" | "range" | "tel" | "url" | "week" | undefinedtype="radio"
name?: string | RawHtml | undefinedname="plan"
value?: string | number | RawHtml | undefinedvalue="free"
"bind:group"?: BindAccessor<string | number | (string | number)[]> | undefinedbind:"bind:group"?: BindAccessor<string | number | (string | number)[]> | undefinedgroup={state: IslandState<MergeState<RootState, "plan", string>>state.plan: SignalAccessor<string>plan}
/>
<"input": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLInputElement> & InputAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
input
type?: "number" | RawHtml | "button" | "search" | "time" | "image" | "text" | "reset" | "submit" | "hidden" | "radio" | "checkbox" | "color" | "date" | "datetime-local" | "email" | "file" | "month" | "password" | "range" | "tel" | "url" | "week" | undefinedtype="radio"
name?: string | RawHtml | undefinedname="plan"
value?: string | number | RawHtml | undefinedvalue="pro"
"bind:group"?: BindAccessor<string | number | (string | number)[]> | undefinedbind:"bind:group"?: BindAccessor<string | number | (string | number)[]> | undefinedgroup={state: IslandState<MergeState<RootState, "plan", string>>state.plan: SignalAccessor<string>plan}
/>
</>
));
Nested fields with .select()
Bind to a nested slice of an object or array with .select() instead of replacing the whole value on every keystroke:
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;
... 6 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha } from "ilha";
const const Profile: Island<RootInput, MergeState<RootState, "user", {
name: string;
role: string;
}>>
Profile = 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;
... 6 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<{
name: string;
role: string;
}, "user">(key: "user", init?: StateInit<RootInput, {
name: string;
role: string;
}> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "user", {
name: string;
role: string;
}>, RootDerived, RootActions>
state("user", { name: stringname: "Ada", role: stringrole: "dev" })
.IlhaBuilder<RootInput, MergeState<RootState, "user", { name: string; role: string; }>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "user", {
name: string;
role: string;
}>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "user", {
name: string;
role: string;
}>>
render(({ state: IslandState<MergeState<RootState, "user", {
name: string;
role: string;
}>>
state }) => (
<"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>
<"input": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLInputElement> & InputAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
input "bind:value"?: BindAccessor<string | number> | undefinedbind:"bind:value"?: BindAccessor<string | number> | undefinedvalue={state: IslandState<MergeState<RootState, "user", {
name: string;
role: string;
}>>
state.user: SignalAccessor<{
name: string;
role: string;
}>
user.MarkedSignalAccessor<{ name: string; role: string; }>.select<string>(selector: (state: {
name: string;
role: string;
}) => string): MarkedSignalAccessor<string>
select((u: {
name: string;
role: string;
}
u) => u: {
name: string;
role: string;
}
u.name: stringname)} />
<"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{state: IslandState<MergeState<RootState, "user", {
name: string;
role: string;
}>>
state.user: MarkedSignalAccessor
() => {
name: string;
role: string;
} (+1 overload)
user().name: stringname}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>
</"div": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLDivElement>>div>
));
In a list, select the item field inside .map():
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;
... 6 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha } from "ilha";
const const Todos: Island<RootInput, MergeState<RootState, "todos", {
text: string;
completed: boolean;
}[]>>
Todos = 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;
... 6 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<{
text: string;
completed: boolean;
}[], "todos">(key: "todos", init?: StateInit<RootInput, {
text: string;
completed: boolean;
}[]> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "todos", {
text: string;
completed: boolean;
}[]>, RootDerived, RootActions>
state("todos", [{ text: stringtext: "Learn ilha", completed: booleancompleted: false }])
.IlhaBuilder<RootInput, MergeState<RootState, "todos", { text: string; completed: boolean; }[]>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "todos", {
text: string;
completed: boolean;
}[]>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "todos", {
text: string;
completed: boolean;
}[]>>
render(({ state: IslandState<MergeState<RootState, "todos", {
text: string;
completed: boolean;
}[]>>
state }) => (
<"ul": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLUListElement>>ul>
{state: IslandState<MergeState<RootState, "todos", {
text: string;
completed: boolean;
}[]>>
state.todos: MarkedSignalAccessor
() => {
text: string;
completed: boolean;
}[] (+1 overload)
todos().Array<{ text: string; completed: boolean; }>.map<JSX.Element>(callbackfn: (value: {
text: string;
completed: boolean;
}, index: number, array: {
text: string;
completed: boolean;
}[]) => JSX.Element, thisArg?: any): JSX.Element[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.map((todo: {
text: string;
completed: boolean;
}
todo, index: numberindex) => (
<"li": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLLIElement> & {
value?: number;
}>
li JSX.IntrinsicAttributes.key?: ((string | number) & (string | number | RawHtml)) | undefinedkey={todo: {
text: string;
completed: boolean;
}
todo.text: stringtext}>
<"input": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLInputElement> & InputAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
input
type?: "number" | RawHtml | "button" | "search" | "time" | "image" | "text" | "reset" | "submit" | "hidden" | "checkbox" | "color" | "date" | "datetime-local" | "email" | "file" | "month" | "password" | "radio" | "range" | "tel" | "url" | "week" | undefinedtype="checkbox"
"bind:checked"?: BindAccessor<boolean> | undefinedbind:"bind:checked"?: BindAccessor<boolean> | undefinedchecked={state: IslandState<MergeState<RootState, "todos", {
text: string;
completed: boolean;
}[]>>
state.todos: SignalAccessor<{
text: string;
completed: boolean;
}[]>
todos.MarkedSignalAccessor<{ text: string; completed: boolean; }[]>.select<boolean>(selector: (state: {
text: string;
completed: boolean;
}[]) => boolean): MarkedSignalAccessor<boolean>
select(
(t: {
text: string;
completed: boolean;
}[]
t) => t: {
text: string;
completed: boolean;
}[]
t[index: numberindex].completed: booleancompleted,
)}
/>
{todo: {
text: string;
completed: boolean;
}
todo.text: stringtext}
</"li": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLLIElement> & {
value?: number;
}>
li>
))}
</"ul": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLUListElement>>ul>
));
The selector must traverse nested state. Writes update only that path — siblings stay intact.
Element references
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;
... 6 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha } from "ilha";
const const Focus: Island<RootInput, MergeState<RootState, "ref", HTMLInputElement | null>>Focus = 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;
... 6 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<RootInput, RootState, RootDerived, RootActions>.state<HTMLInputElement | null, "ref">(key: "ref", init?: StateInit<RootInput, HTMLInputElement | null> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "ref", HTMLInputElement | null>, RootDerived, RootActions>state("ref", null as HTMLInputElement | null)
.IlhaBuilder<RootInput, MergeState<RootState, "ref", HTMLInputElement | null>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "ref", HTMLInputElement | null>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "ref", HTMLInputElement | null>>render(({ state: IslandState<MergeState<RootState, "ref", HTMLInputElement | null>>state }) => <"input": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLInputElement> & InputAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
input "bind:this"?: ElementRefAccessor<HTMLInputElement> | undefinedbind:"bind:this"?: ElementRefAccessor<HTMLInputElement> | undefinedthis={state: IslandState<MergeState<RootState, "ref", HTMLInputElement | null>>state.ref: SignalAccessor<HTMLInputElement | null>ref} />);
Component contracts
A custom component must relay bind:* props to its controller — ilha only wires bindings placed directly on native elements rendered by that island.
Keep these paths separate:
- User input writes the bound signal once and calls the semantic callback once.
- A programmatic signal write updates the controller without calling the semantic callback.
- A controlled prop update changes the controller without masquerading as user input.
On initial mount, ilha reflects bound properties before .onMount(), then attaches native bind listeners afterward. Register component operations with .action(); use direct lowercase event props or a morph-safe .on() selector to call them. Do not pass function handlers through attribute serializers or raw() — those paths produce markup, not live event registrations.