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

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
from "ilha";
const
const Island: Island<Record<string, unknown>, Record<string, never>>
Island
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.transition(opts: TransitionOptions): IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>
transition
({
TransitionOptions.enter?: ((host: Element) => Promise<void> | void) | undefined
enter
: async (
host: Element
host
) => {
await
host: Element
host
.
Animatable.animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation
animate
(
[ {
opacity: number
opacity
: 0,
}, {
opacity: number
opacity
: 1 },
], {
EffectTiming.duration?: string | number | CSSNumericValue | undefined
duration
: 300,
EffectTiming.fill?: FillMode | undefined
fill
: "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

finished
;
},
TransitionOptions.leave?: ((host: Element) => Promise<void> | void) | undefined
leave
: async (
host: Element
host
) => {
await
host: Element
host
.
Animatable.animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation
animate
(
[ {
opacity: number
opacity
: 1,
}, {
opacity: number
opacity
: 0 },
], {
EffectTiming.duration?: string | number | CSSNumericValue | undefined
duration
: 300,
EffectTiming.fill?: FillMode | undefined
fill
: "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

finished
;
}, }) .
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<string, never>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<string, never>>
render
(() => `<div>content</div>`);

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<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
from "ilha";
const
const Island: Island<Record<string, unknown>, Record<string, never>>
Island
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.transition(opts: TransitionOptions): IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>
transition
({
TransitionOptions.enter?: ((host: Element) => Promise<void> | void) | undefined
enter
: (
host: Element
host
) => {
host: Element
host
.
Animatable.animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation
animate
(
[ {
transform: string
transform
: "translateY(8px)",
opacity: number
opacity
: 0 },
{
transform: string
transform
: "none",
opacity: number
opacity
: 1 },
], {
EffectTiming.duration?: string | number | CSSNumericValue | undefined
duration
: 200,
EffectTiming.easing?: string | undefined
easing
: "ease-out" },
); }, }) .
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<string, never>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<string, never>>
render
(() => `<div>content</div>`);

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<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
from "ilha";
const
const Island: Island<Record<string, unknown>, Record<string, never>>
Island
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.transition(opts: TransitionOptions): IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>
transition
({
TransitionOptions.leave?: ((host: Element) => Promise<void> | void) | undefined
leave
: async (
host: Element
host
) => {
await
host: Element
host
.
Animatable.animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation
animate
([{
opacity: number
opacity
: 1 }, {
opacity: number
opacity
: 0 }], {
EffectTiming.duration?: string | number | CSSNumericValue | undefined
duration
: 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

finished
;
}, }) .
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<string, never>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<string, never>>
render
(() => `<div>content</div>`);

If leave throws or rejects, cleanup still runs — the transition error is logged to the console 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<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
from "ilha";
const
const Drawer: Island<Record<string, unknown>, Record<string, never>>
Drawer
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.transition(opts: TransitionOptions): IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>
transition
({
TransitionOptions.enter?: ((host: Element) => Promise<void> | void) | undefined
enter
: async (
host: Element
host
) => {
await
host: Element
host
.
Animatable.animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation
animate
([{
transform: string
transform
: "translateX(-100%)" }, {
transform: string
transform
: "translateX(0)" }], {
EffectTiming.duration?: string | number | CSSNumericValue | undefined
duration
: 250,
EffectTiming.easing?: string | undefined
easing
: "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

finished
;
},
TransitionOptions.leave?: ((host: Element) => Promise<void> | void) | undefined
leave
: async (
host: Element
host
) => {
await
host: Element
host
.
Animatable.animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation
animate
([{
transform: string
transform
: "translateX(0)" }, {
transform: string
transform
: "translateX(-100%)" }], {
EffectTiming.duration?: string | number | CSSNumericValue | undefined
duration
: 250,
EffectTiming.easing?: string | undefined
easing
: "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

finished
;
}, }) .
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<string, never>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<string, never>>
render
(() => `<div class="drawer">content</div>`);

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<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
from "ilha";
function
function cssTransitionEnd(el: Element): Promise<void>
cssTransitionEnd
(
el: Element
el
: Element):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<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.

@paramexecutor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.
Promise
((
resolve: (value: void | PromiseLike<void>) => void
resolve
) => {
el: Element
el
.
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

addEventListener
("transitionend", () =>
resolve: (value: void | PromiseLike<void>) => void
resolve
(), {
AddEventListenerOptions.once?: boolean | undefined
once
: true });
}); } const
const Island: Island<Record<string, unknown>, Record<string, never>>
Island
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, 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;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.transition(opts: TransitionOptions): IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>
transition
({
TransitionOptions.enter?: ((host: Element) => Promise<void> | void) | undefined
enter
: async (
host: Element
host
) => {
host: Element
host
.
Element.classList: DOMTokenList

The 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

classList
.
DOMTokenList.add(...tokens: string[]): void

The add() method of the DOMTokenList interface adds the given tokens to the list, omitting any that are already present.

MDN Reference

add
("is-entering");
await
function cssTransitionEnd(el: Element): Promise<void>
cssTransitionEnd
(
host: Element
host
);
host: Element
host
.
Element.classList: DOMTokenList

The 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

classList
.
DOMTokenList.remove(...tokens: string[]): void

The remove() method of the DOMTokenList interface removes the specified tokens from the list.

MDN Reference

remove
("is-entering");
},
TransitionOptions.leave?: ((host: Element) => Promise<void> | void) | undefined
leave
: async (
host: Element
host
) => {
host: Element
host
.
Element.classList: DOMTokenList

The 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

classList
.
DOMTokenList.add(...tokens: string[]): void

The add() method of the DOMTokenList interface adds the given tokens to the list, omitting any that are already present.

MDN Reference

add
("is-leaving");
await
function cssTransitionEnd(el: Element): Promise<void>
cssTransitionEnd
(
host: Element
host
);
}, }) .
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<string, never>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<string, never>>
render
(() => `<div>content</div>`);

Interaction with .onMount()

The enter transition and .onMount() both run after mount, but in a specific order:

  1. Island mounts and renders into the DOM.
  2. Effects are set up.
  3. .onMount() callbacks run.
  4. Enter transition runs.

This means .onMount() always completes before the enter animation starts.

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.