ilha is a tiny, isomorphic island framework for building reactive UI components.
It lets you render on the server and mount in the browser with fine-grained signal reactivity, without a virtual DOM or compiler overhead. The result is a UI model that stays close to HTML while still giving you state, events, lifecycle hooks, scoped styles, and hydration when you need them.
What ilha is
An island is a self-contained component that knows how to render itself to HTML and how to activate itself in the browser. That means the same component can be used for server-side rendering, client-side mounting, or both together in a hydration flow.
You can start with a plain JSX function, wrap it with ilha() when it needs an independent island boundary, then expand it into the fluent builder when it needs local capabilities.
Why it exists
Most UI stacks force you to choose between simplicity and interactivity. ilha keeps both close together: a small API surface, direct DOM updates through signals, and a rendering model that works naturally on the server.
This makes ilha a good fit when you want:
- Server-rendered markup.
- Small, focused interactive islands.
- Explicit state and behavior.
- No virtual DOM layer.
- A lightweight mental model for UI code.
How it feels to use
A typical island reads a lot like a small HTML-aware module:
The same component can render to a string on the server and mount into the DOM on the client. That keeps the component logic in one place instead of splitting it across separate templates and client scripts.
Choose the smallest component form
Ilha has two runtime modes: transparent plain components and independent islands. Islands have two authoring forms, so you can add capabilities without redesigning your component.
| Start with | Use it when |
|---|---|
const View = () => JSX |
You need reusable markup inside another island |
const View = ilha(() => JSX) |
The component needs an independent mount, hydration, reactive scope, or event lifecycle |
ilha.state(...).action(...).render(...) |
The island needs local state, derived values, actions, input, or lifecycle hooks |
Start plain:
const const Status: () => JSX.ElementStatus = () => <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>Ready</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>;Promote the same render function when it needs an island boundary:
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 Status: Island<RootInput, RootState>Status = ilha<RootInput>(fn: (ctx: RenderContext<RootInput, RootState, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, RootState>ilha(() => <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>Ready</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>);Expand it into the builder when it needs local capabilities:
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 Status: Island<RootInput, MergeState<RootState, "message", string>>Status = 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<string, "message">(key: "message", init?: StateInit<RootInput, string> | undefined): IlhaBuilder<RootInput, MergeState<RootState, "message", string>, RootDerived, RootActions>state("message", "Ready")
.IlhaBuilder<RootInput, MergeState<RootState, "message", string>, RootDerived, RootActions>.render(fn: (ctx: RenderContext<RootInput, MergeState<RootState, "message", string>, RootDerived, RootActions>) => string | RawHtml): Island<RootInput, MergeState<RootState, "message", string>>render(({ state: IslandState<MergeState<RootState, "message", string>>state }) => <"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>{state: IslandState<MergeState<RootState, "message", string>>state.message: MarkedSignalAccessor
() => string (+1 overload)
message()}</"p": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLParagraphElement>>p>);Both ilha(() => JSX) and the builder return the same kind of Island. The shorthand is equivalent to ilha.render(() => JSX).
Core ideas
Isomorphic rendering
ilha can produce HTML on the server and activate the same component in the browser. That makes it useful for SSR, hydration, and progressive enhancement.
Fine-grained reactivity
Signals keep state updates targeted and local. Changing one value does not rerender an entire application tree.
JSX-first authoring
ilha is designed to work naturally with JSX/TSX. The DOM-like syntax is familiar to most developers, and you get full TypeScript support, IDE autocompletion, and standard tooling out of the box.
If you prefer a lower-level option or need to avoid a build step, the html`` tagged-template API is also available. It uses the same runtime and reactivity model, so you can mix both styles or migrate incrementally.
Builder-based composition
The fluent API lets you layer behavior step by step:
.input()for typed props..state()for local reactive state..derived()for computed values.- Native event handlers and
.action()for events and operations. .effect()and.onMount()for side effects..css()for scoped styles..render()to finalize the component.
When to use ilha
ilha is a strong fit when you want:
- Interactive UI with small, explicit components.
- SSR-friendly rendering without heavy framework machinery.
- A simple way to mix server output and client behavior.
- Reusable islands rather than one large application shell.
It is less about building a giant monolithic app framework and more about composing focused UI pieces that each own their own state and behavior.
Basic mental model
Think of an island as a component with three parts:
- Input: data from the outside world.
- State: reactive values owned by the component.
- Render: HTML output driven by that state and input.
Then add behavior on top with events, effects, bindings, and lifecycle hooks. Once you understand that pattern, the rest of the API is mostly a set of focused ways to connect those pieces.