Transition
Attaches enter and leave animation callbacks to the island. The enter callback runs when the island mounts, and the leave callback runs when it unmounts. Both are async — ilha awaits the leave transition before tearing down the island.
Basic usage
The preview mounts a child island (Panel) only while open is true. Each time it appears, enter runs; when you dismiss it, leave runs and ilha waits for that animation to finish before tearing the island down.
A minimal fade on a single island looks like this:
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
const const Island: Island<Record<string, unknown>, Record<never, never>>Island = 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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.transition(opts: TransitionOptions): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>transition({
TransitionOptions.enter?: ((host: Element) => Promise<void> | void) | undefinedenter: async (host: Elementhost) => {
await host: Elementhost.Animatable.animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation[MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animate)animate(
[
{
opacity: numberopacity: 0,
},
{ opacity: numberopacity: 1 },
],
{
EffectTiming.duration?: string | number | CSSNumericValue | undefinedduration: 300,
EffectTiming.fill?: FillMode | undefinedfill: "forwards",
},
).Animation.finished: Promise<Animation>The **`Animation.finished`** read-only property of the Web Animations API returns a Promise which resolves once the animation has finished playing.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Animation/finished)finished;
},
TransitionOptions.leave?: ((host: Element) => Promise<void> | void) | undefinedleave: async (host: Elementhost) => {
await host: Elementhost.Animatable.animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation[MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animate)animate(
[
{
opacity: numberopacity: 1,
},
{ opacity: numberopacity: 0 },
],
{
EffectTiming.duration?: string | number | CSSNumericValue | undefinedduration: 300,
EffectTiming.fill?: FillMode | undefinedfill: "forwards",
},
).Animation.finished: Promise<Animation>The **`Animation.finished`** read-only property of the Web Animations API returns a Promise which resolves once the animation has finished playing.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Animation/finished)finished;
},
})
.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]: anydiv>content</IntrinsicElements[string]: anydiv>);
Enter transition
The enter callback receives the host element immediately after mount. It does not block the island from being interactive — event listeners and effects are already active when it runs.
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
const const Island: Island<Record<string, unknown>, Record<never, never>>Island = 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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.transition(opts: TransitionOptions): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>transition({
TransitionOptions.enter?: ((host: Element) => Promise<void> | void) | undefinedenter: (host: Elementhost) => {
host: Elementhost.Animatable.animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation[MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animate)animate(
[
{ transform: stringtransform: "translateY(8px)", opacity: numberopacity: 0 },
{ transform: stringtransform: "none", opacity: numberopacity: 1 },
],
{ EffectTiming.duration?: string | number | CSSNumericValue | undefinedduration: 200, EffectTiming.easing?: string | undefinedeasing: "ease-out" },
);
},
})
.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]: anydiv>content</IntrinsicElements[string]: anydiv>);
The enter callback does not need to be async if you do not need to await the animation.
Leave transition
The leave callback is awaited before ilha runs cleanup. This means event listeners, effects, and signals remain active for the full duration of the leave animation — state updates and re-renders still work while the island is leaving.
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
const const Island: Island<Record<string, unknown>, Record<never, never>>Island = 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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.transition(opts: TransitionOptions): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>transition({
TransitionOptions.leave?: ((host: Element) => Promise<void> | void) | undefinedleave: async (host: Elementhost) => {
await host: Elementhost.Animatable.animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation[MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animate)animate([{ opacity: numberopacity: 1 }, { opacity: numberopacity: 0 }], {
EffectTiming.duration?: string | number | CSSNumericValue | undefinedduration: 200,
}).Animation.finished: Promise<Animation>The **`Animation.finished`** read-only property of the Web Animations API returns a Promise which resolves once the animation has finished playing.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Animation/finished)finished;
},
})
.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]: anydiv>content</IntrinsicElements[string]: anydiv>);
If leave throws or rejects, cleanup still runs — the error is routed to .onError() / onUncaughtError() with source: "transition" but does not prevent unmounting.
Combining enter and leave
Both callbacks are optional. You can define only one if the other is not needed:
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
const const Drawer: Island<Record<string, unknown>, Record<never, never>>Drawer = 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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.transition(opts: TransitionOptions): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>transition({
TransitionOptions.enter?: ((host: Element) => Promise<void> | void) | undefinedenter: async (host: Elementhost) => {
await host: Elementhost.Animatable.animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation[MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animate)animate(
[
{ transform: stringtransform: "translateX(-100%)" },
{ transform: stringtransform: "translateX(0)" },
],
{
EffectTiming.duration?: string | number | CSSNumericValue | undefinedduration: 250,
EffectTiming.easing?: string | undefinedeasing: "ease-out",
},
).Animation.finished: Promise<Animation>The **`Animation.finished`** read-only property of the Web Animations API returns a Promise which resolves once the animation has finished playing.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Animation/finished)finished;
},
TransitionOptions.leave?: ((host: Element) => Promise<void> | void) | undefinedleave: async (host: Elementhost) => {
await host: Elementhost.Animatable.animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation[MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animate)animate(
[
{ transform: stringtransform: "translateX(0)" },
{ transform: stringtransform: "translateX(-100%)" },
],
{
EffectTiming.duration?: string | number | CSSNumericValue | undefinedduration: 250,
EffectTiming.easing?: string | undefinedeasing: "ease-in",
},
).Animation.finished: Promise<Animation>The **`Animation.finished`** read-only property of the Web Animations API returns a Promise which resolves once the animation has finished playing.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Animation/finished)finished;
},
})
.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]: anydiv class?: string | unknown[] | Record<string, boolean> | undefinedclass="drawer">content</IntrinsicElements[string]: anydiv>);
Using CSS transitions
You are not limited to the Web Animations API. Any async work is valid — including toggling a class and waiting for a CSS transition to finish:
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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha from "ilha";
function function cssTransitionEnd(el: Element): Promise<void>cssTransitionEnd(el: Elementel: Element): interface Promise<T>Represents the completion of an asynchronous operationPromise<void> {
return new var Promise: PromiseConstructor
new <void>(executor: (resolve: (value: void | PromiseLike<void>) => void, reject: (reason?: any) => void) => void) => Promise<void>
Creates a new Promise.Promise((resolve: (value: void | PromiseLike<void>) => voidresolve) => {
el: Elementel.Element.addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void (+1 overload)The **`addEventListener()`** method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)addEventListener("transitionend", () => resolve: (value: void | PromiseLike<void>) => voidresolve(), {
AddEventListenerOptions.once?: boolean | undefinedonce: true,
});
});
}
const const Island: Island<Record<string, unknown>, Record<never, never>>Island = 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;
... 4 more ...;
onUncaughtError: typeof onUncaughtError;
}
ilha
.IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>.transition(opts: TransitionOptions): IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>>transition({
TransitionOptions.enter?: ((host: Element) => Promise<void> | void) | undefinedenter: async (host: Elementhost) => {
host: Elementhost.Element.classList: DOMTokenListThe read-only **`classList`** property of the Element interface contains a live DOMTokenList collection representing the class attribute of the element. This can then be used to manipulate the class list.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/classList)classList.DOMTokenList.add(...tokens: string[]): voidThe **`add()`** method of the DOMTokenList interface adds the given tokens to the list, omitting any that are already present.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMTokenList/add)add("is-entering");
await function cssTransitionEnd(el: Element): Promise<void>cssTransitionEnd(host: Elementhost);
host: Elementhost.Element.classList: DOMTokenListThe read-only **`classList`** property of the Element interface contains a live DOMTokenList collection representing the class attribute of the element. This can then be used to manipulate the class list.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/classList)classList.DOMTokenList.remove(...tokens: string[]): voidThe **`remove()`** method of the DOMTokenList interface removes the specified tokens from the list.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMTokenList/remove)remove("is-entering");
},
TransitionOptions.leave?: ((host: Element) => Promise<void> | void) | undefinedleave: async (host: Elementhost) => {
host: Elementhost.Element.classList: DOMTokenListThe read-only **`classList`** property of the Element interface contains a live DOMTokenList collection representing the class attribute of the element. This can then be used to manipulate the class list.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/classList)classList.DOMTokenList.add(...tokens: string[]): voidThe **`add()`** method of the DOMTokenList interface adds the given tokens to the list, omitting any that are already present.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMTokenList/add)add("is-leaving");
await function cssTransitionEnd(el: Element): Promise<void>cssTransitionEnd(host: Elementhost);
},
})
.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]: anydiv>content</IntrinsicElements[string]: anydiv>);
Mount order
After the first render is in the DOM, slots are mounted, and event listeners are attached, ilha runs client setup in this order:
enter transition (if .transition() is set) — may be sync or async; rejections are reported to .onError() / onUncaughtError() with source: "transition".
.onMount() callbacks (unless skipped via hydration skipOnMount).
.effect() and derived watchers — then the render effect keeps the host in sync with signals.
.effect() callbacks are not registered until after enter and .onMount() finish — nothing in the effect system runs during enter. Use enter for the animation itself; use .onMount() for one-time setup that must run after enter completes (when enter is async, await your animation inside enter before returning). If you need child slots mounted before measuring or animating, the DOM is already rendered and slots are mounted before step 1 — only effects and the ongoing render loop start later.
Notes
- Only one
.transition() call is supported per builder chain. Calling it more than once replaces the previous transition options.
- Transitions are client-side only and are never called during SSR.
- The
leave transition is awaited, so a very long or stalled animation will delay cleanup. Make sure your leave animations have a bounded duration or a timeout.