#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.
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.
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.
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.
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.
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.
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: 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.
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.
add ("is-entering");
await function cssTransitionEnd(el: Element): Promise<void> cssTransitionEnd (host: Element host );
host: Element host .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.
classList .DOMTokenList.remove(...tokens: string[]): voidThe remove() method of the DOMTokenList interface removes the specified tokens from the list.
remove ("is-entering");
},
TransitionOptions.leave?: ((host: Element) => Promise<void> | void) | undefined leave : async (host: Element host ) => {
host: Element host .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.
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.
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:
- Island mounts and renders into the DOM.
- Effects are set up.
.onMount()callbacks run.- 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
leavetransition is awaited, so a very long or stalled animation will delay cleanup. Make sure your leave animations have a bounded duration or a timeout.