Transitions
Svelte transitions and animations for smooth enter/exit effects and layout changes
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 linesTransitions 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:fadeon 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
slideon elements inside flex or grid containers where theheightandoverflowanimation conflicts with the parent layout, causing visual jumps. Test carefully or useflyas 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
onMountor checkbrowserfrom$app/environmentfor 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
| Transition | Effect |
|---|---|
fade | Opacity 0 to 1 |
fly | Translate + fade from offset position |
slide | Collapse/expand height |
scale | Scale + fade |
blur | Blur + fade |
draw | SVG path stroke animation |
crossfade | Paired 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
cssinstead oftick) for better performance — they run on the compositor thread and do not block the main thread. - Use keyed
{#each}withanimate:flipfor list reordering. Without keys, Svelte cannot track which elements moved. - Pair
crossfadewithanimate:flipfor 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|globalwhen 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
slideon flex/grid children.slideanimatesheightandoverflow, which can conflict with flex layouts. Test carefully or useflyinstead. - Transition + conditional class conflicts. Avoid changing an element's
displayproperty 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'sbrowseror useonMount.
Install this skill directly: skilldb add svelte-skills
Related Skills
Component Patterns
Svelte component composition patterns including props, snippets, context, and advanced reuse techniques
Form Actions
SvelteKit form actions for progressive enhancement with server-side form handling
Load Functions
SvelteKit server and universal load functions for fetching and passing data to pages and layouts
Reactivity
Svelte 5 runes system for fine-grained reactivity with $state, $derived, and $effect
Stores
Svelte stores and state management patterns including writable, readable, derived, and custom stores
Sveltekit Auth
Authentication patterns in SvelteKit using hooks, cookies, sessions, and OAuth flows