View Transitions
You animate between UI states with the browser’s native View Transition API. Ilha
doesn’t wrap it — you call document.startViewTransition() and update state
inside the callback.
import { const ilha: IlhaFactoryilha, function state<T>(init?: T | (() => T)): StateAccessor<T>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state } from "ilha";
export default ilha<unknown>(component: IslandComponent<unknown>): Island<unknown> (+3 overloads)ilha(() => {
const const checked: StateAccessor<boolean>checked = state<boolean>(init?: boolean | (() => boolean) | undefined): StateAccessor<boolean>Declare island-local reactive state at this call position. The initializer
applies only when the instance is created — later renders reuse the same
underlying signal, so prop-driven initializers never reset user state.
A function argument is treated as a lazy initializer:
const count = state(() => expensiveInitialValue());
To store a function VALUE, return it from the updater wrapper on write:
setCallback(() => nextCallback);state(false);
const const toggle: () => voidtoggle = () => {
var document: Document**`window.document`** returns a reference to the document contained in the window.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)document.Document.startViewTransition(callbackOptions?: ViewTransitionUpdateCallback | StartViewTransitionOptions): ViewTransitionThe **`startViewTransition()`** method of the Document interface starts a new same-document (SPA) view transition and returns a ViewTransition object to represent it.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/startViewTransition)startViewTransition(() => {
const checked: MarkedSignalAccessor
(value: SignalSetter<boolean>) => void (+1 overload)
checked(!const checked: MarkedSignalAccessor
() => boolean (+1 overload)
checked());
});
};
return (
<"button": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLButtonElement> & ButtonAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
button
class?: RawHtml | ClassValue | undefinedclass="card"
onclick?: NativeEventHandler<PointerEvent & {
readonly currentTarget: HTMLButtonElement;
}> | undefined
onclick={const toggle: () => voidtoggle}
style?: string | RawHtml | StyleProps | undefinedstyle="view-transition-name: card"
>
{const checked: MarkedSignalAccessor
() => boolean (+1 overload)
checked() ? "Checked" : "Unchecked"}
</"button": WithRawHtmlAttributeValues<JSX.HTMLAttributes<HTMLButtonElement> & ButtonAttributes & {
popovertarget?: string;
popoverTarget?: string;
popovertargetaction?: "hide" | "show" | "toggle";
popoverTargetAction?: "hide" | "show" | "toggle";
}>
button>
);
});
How it works
startViewTransition(callback) snapshots the current page, runs callback
(where you write new state), then cross-fades between the old and new snapshots.
You wrap only the state write — the island re-renders and morph() patches the
DOM inside the transition, so the browser animates the change.
Name shared elements
The same element before and after the change must carry the same
view-transition-name for the browser to morph it.
.card {
view-transition-name: card;
}
Respect the reduced-motion setting
import { state } from "ilha";
const checked = state(false);
const toggle = () => {
const reduce = matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
const write = () => checked(!checked());
if (reduce) {
write();
} else {
document.startViewTransition(write);
}
});