Define
Registers the island as a custom element — usable from plain HTML, or from any framework that can render arbitrary tags, without touching ilha's mount() API at all. This is one of the easiest ways to drop an ilha island into an existing app, a CMS template, or a static page.
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;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}ilha from "ilha";
const const Badge: Island<{
label: string;
}, Record<never, never>>Badge = 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;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.input<{
label: string;
}>(defaults: {
label: string;
}): IlhaBuilder<{
label: string;
}, Record<never, never>, Record<never, never>> (+2 overloads)input<{ label: stringlabel: string }>({ label: stringlabel: "?" })
.IlhaBuilder<{ label: string; }, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<{
label: string;
}, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<{
label: string;
}, Record<never, never>>render(({ input: {
label: string;
}input }) => <IntrinsicElements[string]: anyspan>{input: {
label: string;
}input.label: stringlabel}</IntrinsicElements[string]: anyspan>);
const Badge: Island<{
label: string;
}, Record<never, never>>Badge.Island<{ label: string; }, Record<never, never>>.define(tagName: string, options?: {
observe?: string[];
}): voidRegister this island as a custom element, usable from plain HTML or any
framework: `Counter.define("x-counter", { observe: ["label"] })` then
`<x-counter label="hi"></x-counter>`. Observed attributes become string
input props and re-resolve input on change; richer props can be assigned
via the element's `props` property. Mounts on connect, unmounts on
disconnect. No-op (with a dev warning) where customElements is missing.define("x-ilha-badge", { observe?: string[] | undefinedobserve: ["label"] });
<x-ilha-badge label="hello"></x-ilha-badge>
The element mounts the island as soon as it connects to the DOM, and unmounts it when it disconnects — no explicit mount() call needed anywhere.
Options
interface DefineOptions {
observe?: string[]; // attribute names to watch and pass through as string props
}
Observed attributes
Attributes named in observe become string input props. Changing the attribute re-resolves input and re-renders the island automatically:
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;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}ilha from "ilha";
const const Counter: Island<{
start: string;
}, 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;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.input<{
start: string;
}>(defaults: {
start: string;
}): IlhaBuilder<{
start: string;
}, Record<never, never>, Record<never, never>> (+2 overloads)input<{ start: stringstart: string }>({ start: stringstart: "0" })
.IlhaBuilder<{ start: string; }, Record<never, never>, Record<never, never>>.state<number, "count">(key: "count", init?: StateInit<{
start: string;
}, number> | undefined): IlhaBuilder<{
start: string;
}, MergeState<Record<never, never>, "count", number>, Record<never, never>>state("count", ({ start: stringstart }) => var Number: NumberConstructor
(value?: any) => numberAn object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.Number(start: stringstart))
.IlhaBuilder<{ start: string; }, MergeState<Record<never, never>, "count", number>, Record<never, never>>.render(fn: (ctx: RenderContext<{
start: string;
}, MergeState<Record<never, never>, "count", number>, Record<never, never>>) => string | RawHtml): Island<{
start: string;
}, 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 Counter: Island<{
start: string;
}, MergeState<Record<never, never>, "count", number>>Counter.Island<{ start: string; }, MergeState<Record<never, never>, "count", number>>.define(tagName: string, options?: {
observe?: string[];
}): voidRegister this island as a custom element, usable from plain HTML or any
framework: `Counter.define("x-counter", { observe: ["label"] })` then
`<x-counter label="hi"></x-counter>`. Observed attributes become string
input props and re-resolve input on change; richer props can be assigned
via the element's `props` property. Mounts on connect, unmounts on
disconnect. No-op (with a dev warning) where customElements is missing.define("x-counter", { observe?: string[] | undefinedobserve: ["start"] });
<x-counter start="10"></x-counter>
<script>
document
.querySelector("x-counter")
.setAttribute("start", "20");
</script>
Because HTML attributes are always strings, coerce them to the type you need (as Number(start) does above) inside .state() or .derived().
Rich props via the props property
Attributes can only carry strings. For objects, arrays, or other non-string props, assign them directly to the element's props property — set from any JavaScript, including another framework's component:
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;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}ilha from "ilha";
const const List: Island<{
items: string[];
}, Record<never, never>>List = 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;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.input<{
items: string[];
}>(defaults: {
items: string[];
}): IlhaBuilder<{
items: string[];
}, Record<never, never>, Record<never, never>> (+2 overloads)input<{ items: string[]items: string[] }>({ items: string[]items: [] })
.IlhaBuilder<{ items: string[]; }, Record<never, never>, Record<never, never>>.render(fn: (ctx: RenderContext<{
items: string[];
}, Record<never, never>, Record<never, never>>) => string | RawHtml): Island<{
items: string[];
}, Record<never, never>>render(({ input: {
items: string[];
}input }) => (
<IntrinsicElements[string]: anyul>
{input: {
items: string[];
}input.items: string[]items.Array<string>.map<JSX.Element>(callbackfn: (value: string, index: number, array: string[]) => 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((i: stringi) => (
<IntrinsicElements[string]: anyli>{i: stringi}</IntrinsicElements[string]: anyli>
))}
</IntrinsicElements[string]: anyul>
));
const List: Island<{
items: string[];
}, Record<never, never>>List.Island<{ items: string[]; }, Record<never, never>>.define(tagName: string, options?: {
observe?: string[];
}): voidRegister this island as a custom element, usable from plain HTML or any
framework: `Counter.define("x-counter", { observe: ["label"] })` then
`<x-counter label="hi"></x-counter>`. Observed attributes become string
input props and re-resolve input on change; richer props can be assigned
via the element's `props` property. Mounts on connect, unmounts on
disconnect. No-op (with a dev warning) where customElements is missing.define("x-ilha-list");
const host = document.createElement(
"x-ilha-list",
) as HTMLElement & {
props?: { items: string[] };
};
host.props = { items: ["a", "b"] };
document.body.appendChild(host);
// Later updates re-render the island:
host.props = { items: ["a", "b", "c"] };
props is merged over attribute-derived props, so you can combine both — observed attributes for simple values, props for anything richer.
Falling back to data-ilha-props
If neither observe attributes nor props are set, the element falls back to reading data-ilha-props off itself — the same serialized-props convention used by .hydratable():
<x-ilha-badge
data-ilha-props='{"label":"hello"}'
></x-ilha-badge>
Using from another framework
Because the custom element mounts and unmounts itself via the standard connectedCallback / disconnectedCallback lifecycle, it works anywhere custom elements are supported — React, Vue, Svelte, plain HTML, or a CMS template — with no ilha-specific glue code required in the host framework.
// Inside a React component, for example:
function Page() {
return <x-ilha-badge label="from React" />;
}
SSR safety
.define() is a no-op with a dev warning where customElements is unavailable (for example, during SSR in a Node environment):
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;
... 5 more ...;
onUncaughtError: typeof onUncaughtError;
}ilha from "ilha";
const const Badge: Island<Record<string, unknown>, Record<never, never>>Badge = 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;
... 5 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]: anyspan>hi</IntrinsicElements[string]: anyspan>);
// Safe to call during module init even on the server —
// logs a dev warning and returns without registering anything.
const Badge: Island<Record<string, unknown>, Record<never, never>>Badge.Island<Record<string, unknown>, Record<never, never>>.define(tagName: string, options?: {
observe?: string[];
}): voidRegister this island as a custom element, usable from plain HTML or any
framework: `Counter.define("x-counter", { observe: ["label"] })` then
`<x-counter label="hi"></x-counter>`. Observed attributes become string
input props and re-resolve input on change; richer props can be assigned
via the element's `props` property. Mounts on connect, unmounts on
disconnect. No-op (with a dev warning) where customElements is missing.define("x-ilha-badge");
Notes
- Registering the same tag name twice logs a dev warning and skips the second registration —
customElements.define()throws if called twice for the same tag, so.define()guards against that. .define()can be called anywhere after.render()finalizes the builder chain — typically once at module scope, next to the island's definition.- Combine
.define()with.hydratable()if you also render the island server-side elsewhere — they are independent integration paths for the same island.