Skip to main content
Technology & EngineeringSvelte293 lines

Transitions

Svelte transitions and animations for smooth enter/exit effects and layout changes

Quick Summary26 lines
You are an expert in Svelte's transition and animation system, helping developers create smooth, performant UI animations using built-in directives and custom transition functions.

## Key Points

- **Prefer CSS-based transitions** (return `css` instead of `tick`) for better performance — they run on the compositor thread and do not block the main thread.
- **Use keyed `{#each}` with `animate:flip`** for list reordering. Without keys, Svelte cannot track which elements moved.
- **Pair `crossfade` with `animate:flip`** for full list + movement animations.
- **Keep durations short** (200-400ms) for UI interactions. Longer animations feel sluggish.
- **Use `|local` (default)** to avoid unexpected animations when parent blocks toggle. Only add `|global` when you explicitly want it.
- **Missing key in `{#each}` blocks.** Without `(item.id)`, transitions fire for every element on every list change, causing visual chaos.
- **Transitions on component roots.** Transitions must be on native HTML elements, not Svelte components. Wrap the component's root element with the transition.
- **Height transitions with `slide` on flex/grid children.** `slide` animates `height` and `overflow`, which can conflict with flex layouts. Test carefully or use `fly` instead.
- **Transition + conditional class conflicts.** Avoid changing an element's `display` property via CSS classes while a transition is active — it can cancel the animation.
- **SSR and transitions.** Intro transitions do not play during SSR hydration by default. If you need a page-load animation, check `$app/environment`'s `browser` or use `onMount`.

## Quick Example

```svelte
{#if outer}
  {#if inner}
    <div transition:fade|global>Always animates</div>
  {/if}
{/if}
```
skilldb get svelte-skills/TransitionsFull skill: 293 lines
Paste into your CLAUDE.md or agent config

Transitions and Animations — Svelte

You are an expert in Svelte's transition and animation system, helping developers create smooth, performant UI animations using built-in directives and custom transition functions.

Core Philosophy

Svelte treats animation as a first-class concern, not an afterthought bolted on through a CSS library or a third-party package. The transition:, in:, out:, and animate: directives are part of the template language because transitions are fundamentally a rendering concern — they describe how elements appear, disappear, and move within the document. By integrating animations into the compiler, Svelte can orchestrate enter and exit transitions, handle interruptions gracefully, and generate CSS animations that run on the compositor thread without blocking the main thread.

The design philosophy is progressive complexity. A basic transition:fade requires one import and one directive. Separate in: and out: directives let you use different effects for entering and leaving. crossfade handles shared element transitions between lists. Custom transition functions give you full control over CSS or JavaScript-driven animations. Each step adds capability without requiring you to understand the steps above it. Most applications only need the first two levels.

Performance is built into the system by default. CSS-based transitions (those returning a css function) are more performant than JavaScript-based ones (those using tick) because they run off the main thread. The built-in transitions all use CSS. Custom transitions should prefer the css approach and only fall back to tick for effects that genuinely require JavaScript DOM manipulation each frame, such as typewriter effects.

Anti-Patterns

  • Missing Keys in {#each} Blocks with Transitions — using {#each items as item} without (item.id) when transitions are applied. Without keys, Svelte cannot track which elements moved, causing every element to re-transition on any list change.

  • Applying Transitions to Svelte Components — placing transition:fade on a <MyComponent> tag instead of on a native HTML element inside it. Transitions must be on native DOM elements; they silently fail on component tags.

  • Long Animation Durations on UI Interactions — setting transition durations above 400ms for interactive elements like buttons, modals, and dropdowns. Animations longer than 400ms feel sluggish and frustrate users. Reserve long durations for page-level transitions or decorative effects.

  • Height Transitions on Flex/Grid Children — using slide on elements inside flex or grid containers where the height and overflow animation conflicts with the parent layout, causing visual jumps. Test carefully or use fly as an alternative.

  • Relying on Intro Transitions During SSR Hydration — expecting enter transitions to play when the page first loads via server rendering. Intro transitions do not play during hydration by default. Use onMount or check browser from $app/environment for page-load animations.

Overview

Svelte provides first-class support for animations through three mechanisms: transitions (enter/exit effects triggered by {#if} and {#each}), animations (layout shifts in keyed {#each} blocks), and motion utilities (tweened and spring stores for interpolating values). All are declarative, composable, and optimized to use CSS animations where possible.

Core Concepts

Basic Transitions

Apply a transition with the transition: directive. The element animates in when added to the DOM and out when removed.

<script>
  import { fade } from 'svelte/transition';
  let visible = $state(true);
</script>

<button onclick={() => visible = !visible}>Toggle</button>

{#if visible}
  <p transition:fade>Hello!</p>
{/if}

Transition Parameters

Most built-in transitions accept configuration:

<script>
  import { fly } from 'svelte/transition';
</script>

{#if visible}
  <div transition:fly={{ y: 200, duration: 400, delay: 100 }}>
    Flies in from below
  </div>
{/if}

Separate In/Out Transitions

Use in: and out: for different enter and exit animations:

<script>
  import { fly, fade } from 'svelte/transition';
</script>

{#if visible}
  <div in:fly={{ y: -20 }} out:fade>
    Flies in, fades out
  </div>
{/if}

Built-in Transitions

TransitionEffect
fadeOpacity 0 to 1
flyTranslate + fade from offset position
slideCollapse/expand height
scaleScale + fade
blurBlur + fade
drawSVG path stroke animation
crossfadePaired send/receive for element movement

Transition Events

Listen for transition lifecycle events:

<div
  transition:fly={{ y: 200 }}
  onintrostart={() => console.log('intro started')}
  onintroend={() => console.log('intro ended')}
  onoutrostart={() => console.log('outro started')}
  onoutroend={() => console.log('outro ended')}
>
  Animated element
</div>

|global and |local Modifiers

By default, transitions only play when the direct parent block ({#if}, {#each}) is toggled. Use |global to play when any ancestor block changes:

{#if outer}
  {#if inner}
    <div transition:fade|global>Always animates</div>
  {/if}
{/if}

Implementation Patterns

Crossfade (Shared Element Transitions)

Create paired send/receive transitions for elements that "move" between locations:

<script>
  import { crossfade } from 'svelte/transition';
  import { quintOut } from 'svelte/easing';

  const [send, receive] = crossfade({
    duration: 400,
    easing: quintOut,
    fallback: fade
  });

  let todos = $state([
    { id: 1, text: 'Learn Svelte', done: false },
    { id: 2, text: 'Build app', done: false }
  ]);

  let active = $derived(todos.filter(t => !t.done));
  let completed = $derived(todos.filter(t => t.done));

  function toggle(id) {
    const todo = todos.find(t => t.id === id);
    if (todo) todo.done = !todo.done;
  }
</script>

<div class="board">
  <div class="column">
    <h2>Active</h2>
    {#each active as todo (todo.id)}
      <div
        in:receive={{ key: todo.id }}
        out:send={{ key: todo.id }}
      >
        <button onclick={() => toggle(todo.id)}>{todo.text}</button>
      </div>
    {/each}
  </div>

  <div class="column">
    <h2>Done</h2>
    {#each completed as todo (todo.id)}
      <div
        in:receive={{ key: todo.id }}
        out:send={{ key: todo.id }}
      >
        <button onclick={() => toggle(todo.id)}>{todo.text}</button>
      </div>
    {/each}
  </div>
</div>

Custom Transitions

Write your own transition by returning a CSS animation object:

function typewriter(node, { speed = 1 }) {
  const text = node.textContent;
  const duration = text.length / (speed * 0.01);

  return {
    duration,
    tick(t) {
      const i = Math.trunc(text.length * t);
      node.textContent = text.slice(0, i);
    }
  };
}

CSS-based custom transition (more performant):

function whoosh(node, { duration = 400 }) {
  const o = +getComputedStyle(node).opacity;

  return {
    duration,
    css: (t) => `
      opacity: ${t * o};
      transform: scale(${t}) rotate(${(1 - t) * 360}deg);
    `
  };
}

animate:flip for List Reordering

The animate directive smoothly transitions elements when their position changes in a keyed {#each}:

<script>
  import { flip } from 'svelte/animate';

  let items = $state([
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
    { id: 3, name: 'Carol' }
  ]);

  function shuffle() {
    items = items.sort(() => Math.random() - 0.5);
  }
</script>

<button onclick={shuffle}>Shuffle</button>

{#each items as item (item.id)}
  <div animate:flip={{ duration: 300 }}>
    {item.name}
  </div>
{/each}

Motion Utilities: Tweened and Spring

For animating numeric or interpolatable values over time:

<script>
  import { tweened } from 'svelte/motion';
  import { cubicOut } from 'svelte/easing';

  const progress = tweened(0, { duration: 600, easing: cubicOut });
</script>

<button onclick={() => progress.set(100)}>Complete</button>

<div class="bar" style="width: {$progress}%"></div>
<script>
  import { spring } from 'svelte/motion';

  const coords = spring({ x: 0, y: 0 }, {
    stiffness: 0.1,
    damping: 0.25
  });
</script>

<svelte:window onpointermove={(e) => coords.set({ x: e.clientX, y: e.clientY })} />

<div class="cursor" style="left: {$coords.x}px; top: {$coords.y}px" />

Best Practices

  • Prefer CSS-based transitions (return css instead of tick) for better performance — they run on the compositor thread and do not block the main thread.
  • Use keyed {#each} with animate:flip for list reordering. Without keys, Svelte cannot track which elements moved.
  • Pair crossfade with animate:flip for full list + movement animations.
  • Keep durations short (200-400ms) for UI interactions. Longer animations feel sluggish.
  • Use |local (default) to avoid unexpected animations when parent blocks toggle. Only add |global when you explicitly want it.

Common Pitfalls

  • Missing key in {#each} blocks. Without (item.id), transitions fire for every element on every list change, causing visual chaos.
  • Transitions on component roots. Transitions must be on native HTML elements, not Svelte components. Wrap the component's root element with the transition.
  • Height transitions with slide on flex/grid children. slide animates height and overflow, which can conflict with flex layouts. Test carefully or use fly instead.
  • Transition + conditional class conflicts. Avoid changing an element's display property via CSS classes while a transition is active — it can cancel the animation.
  • SSR and transitions. Intro transitions do not play during SSR hydration by default. If you need a page-load animation, check $app/environment's browser or use onMount.

Install this skill directly: skilldb add svelte-skills

Get CLI access →