---
title: .transition()
description: Attach enter and leave animation callbacks to islands for async mount and unmount transitions.
order: 208
---

import { Preview } from "$lib/components/preview";

export const example = `import ilha from "ilha";
import { Button } from "areia";

const Panel = ilha
.transition({
enter: async (host) => {
await host.animate(
[
{
opacity: 0,
transform: "translateY(12px) scale(0.98)",
},
{
opacity: 1,
transform: "translateY(0) scale(1)",
},
],
{ duration: 300, easing: "ease-out", fill: "forwards" },
).finished;
},
leave: async (host) => {
await host.animate(
[
{
opacity: 1,
transform: "translateY(0) scale(1)",
},
{
opacity: 0,
transform: "translateY(-8px) scale(0.98)",
},
],
{
duration: 240,
easing: "ease-in",
fill: "forwards",
},
).finished;
},
})
.render(() => (

<div class="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-green-900">
  <p class="font-medium">Animated panel</p>
  <p class="mt-1 text-sm text-green-800">
    Fades and slides in on mount; animates out before unmount.
  </p>
</div>
));

export default ilha
  .state("open", true)
  .on("button@click", ({ state }) => state.open(!state.open()))
  .render(({ state }) => (
    <div class="flex min-h-52 flex-col gap-4">
      <Button variant="outline">
        {state.open() ? "Dismiss panel" : "Show panel"}
      </Button>
      {state.open() ? <Panel /> : null}
      <p class="text-sm text-areia-muted">
        Toggle to replay enter and leave; leave is awaited before cleanup runs.
      </p>
    </div>
  ));
`

# 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

<Preview code={example} size="lg" />

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:

```tsx twoslash
import ilha from "ilha";

const Island = ilha
  .transition({
    enter: async (host) => {
      await host.animate(
        [
          {
            opacity: 0,
          },
          { opacity: 1 },
        ],
        {
          duration: 300,
          fill: "forwards",
        },
      ).finished;
    },
    leave: async (host) => {
      await host.animate(
        [
          {
            opacity: 1,
          },
          { opacity: 0 },
        ],
        {
          duration: 300,
          fill: "forwards",
        },
      ).finished;
    },
  })
  .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.

```tsx twoslash
import ilha from "ilha";

const Island = ilha
  .transition({
    // [!code highlight:9]
    enter: (host) => {
      host.animate(
        [
          { transform: "translateY(8px)", opacity: 0 },
          { transform: "none", opacity: 1 },
        ],
        { duration: 200, easing: "ease-out" },
      );
    },
  })
  .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.

```tsx twoslash
import ilha from "ilha";

const Island = ilha
  .transition({
    // [!code highlight:4]
    leave: async (host) => {
      await host.animate([{ opacity: 1 }, { opacity: 0 }], {
        duration: 200,
      }).finished;
    },
  })
  .render(() => <div>content</div>);
```

If `leave` throws or rejects, cleanup still runs — the error is routed to [`.onError()` / `onUncaughtError()`](/guide/island/onerror) 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:

```tsx twoslash
import ilha from "ilha";

const Drawer = ilha
  .transition({
    enter: async (host) => {
      await host.animate(
        [
          { transform: "translateX(-100%)" },
          { transform: "translateX(0)" },
        ],
        {
          duration: 250,
          easing: "ease-out",
        },
      ).finished;
    },
    leave: async (host) => {
      await host.animate(
        [
          { transform: "translateX(0)" },
          { transform: "translateX(-100%)" },
        ],
        {
          duration: 250,
          easing: "ease-in",
        },
      ).finished;
    },
  })
  .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:

```tsx twoslash
import ilha from "ilha";

function cssTransitionEnd(el: Element): Promise<void> {
  return new Promise((resolve) => {
    el.addEventListener("transitionend", () => resolve(), {
      once: true,
    });
  });
}

const Island = ilha
  .transition({
    enter: async (host) => {
      host.classList.add("is-entering");
      await cssTransitionEnd(host);
      host.classList.remove("is-entering");
    },
    leave: async (host) => {
      host.classList.add("is-leaving");
      await cssTransitionEnd(host);
    },
  })
  .render(() => <div>content</div>);
```

## 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:

1. **`enter` transition** (if [`.transition()`](/guide/island/transition) is set) — may be sync or async; rejections are reported to [`.onError()` / `onUncaughtError()`](/guide/island/onerror) with `source: "transition"`.
2. **[`.onMount()`](/guide/island/onmount)** callbacks (unless skipped via hydration `skipOnMount`).
3. **[`.effect()`](/guide/island/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.
