Transitions
Svelte ships built-in transitions for entering and leaving the DOM — fade, slide, fly, scale, crossfade, blur — with sensible defaults and zero-config easing. Add motion that respects user preferences without pulling in an animation library.
transition, animate, key blocks, motion
EXAMPLE
<script>
import { fade, slide, fly, scale, blur, crossfade } from 'svelte/transition';
import { cubicOut, elasticOut, quintInOut } from 'svelte/easing';
import { flip } from 'svelte/animate';
import { writable } from 'svelte/store';
let visible = false;
let items = [
{ id: 1, text: 'apple' },
{ id: 2, text: 'banana' },
{ id: 3, text: 'cherry' },
];
</script>
<!-- 1) The simplest transition — fade in/out -->
<button on:click={() => (visible = !visible)}>Toggle</button>
{#if visible}
<p transition:fade>I fade in and out.</p>
{/if}
<!-- 2) Different on enter vs leave -->
{#if visible}
<p in:fly={{ y: 20, duration: 300 }} out:fade>One direction in, fade out</p>
{/if}
<!-- 3) Customising parameters + easing -->
{#if visible}
<p
transition:fly={{
y: 50,
duration: 400,
delay: 100,
easing: cubicOut,
}}
>
Bounce up
</p>
{/if}
<!-- 4) Lists with stable keys -->
{#each items as item (item.id)}
<div
in:fade={{ duration: 200 }}
out:slide={{ duration: 200 }}
animate:flip={{ duration: 250 }}
>
{item.text}
</div>
{/each}
<!-- animate:flip animates LAYOUT changes (reorder) — pair with key. -->
<!-- 5) Crossfade — element 'moves' between locations -->
<script>
const [send, receive] = crossfade({
duration: 250,
fallback(node, params) {
const style = getComputedStyle(node);
return {
duration: 200,
css: (t) => `opacity: ${t}; transform: scale(${0.95 + 0.05 * t})`,
};
},
});
let left = items;
let right = [];
function move(item, from, to) {
from.splice(from.indexOf(item), 1);
to.push(item);
left = left; // trigger reactivity
right = right;
}
</script>
<div class="columns">
<ul>
{#each left as item (item.id)}
<li in:receive={{ key: item.id }} out:send={{ key: item.id }} on:click={() => move(item, left, right)}>
{item.text}
</li>
{/each}
</ul>
<ul>
{#each right as item (item.id)}
<li in:receive={{ key: item.id }} out:send={{ key: item.id }} on:click={() => move(item, right, left)}>
{item.text}
</li>
{/each}
</ul>
</div>
<!-- The element appears to glide between the two columns. -->
<!-- 6) Key blocks — transition on value change -->
<script>
let count = 0;
</script>
<button on:click={() => (count += 1)}>+1</button>
{#key count}
<span in:fly={{ y: -20, duration: 200 }}>{count}</span>
{/key}
<!-- The span unmounts + remounts every time count changes, triggering the transition. -->
<!-- 7) Custom transitions -->
<script>
function typewriter(node, { speed = 1 } = {}) {
const valid = node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE;
if (!valid) throw new Error('typewriter expects a single text-node child');
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);
},
};
}
</script>
{#if visible}
<p in:typewriter={{ speed: 1 }}>The quick brown fox</p>
{/if}
<!-- 8) Transition events — onstart, onend, onintroend, onoutrostart -->
{#if visible}
<p
transition:fade
on:introstart={() => console.log('enter start')}
on:introend={() => console.log('enter end')}
on:outrostart={() => console.log('exit start')}
on:outroend={() => console.log('exit end')}
>
Logged
</p>
{/if}
<!-- 9) Local vs global — by default, transitions only run on the element entering/leaving the IF/EACH BOUNDARY -->
<!-- |global lets them run on any parent change -->
{#if visible}
<p transition:fade|global>I run when ANY ancestor toggles, not just visible</p>
{/if}
<!-- 10) Respect prefers-reduced-motion -->
<script>
import { reducedMotion } from './stores'; // window.matchMedia('(prefers-reduced-motion: reduce)')
</script>
{#if visible}
<p transition:fade={{ duration: $reducedMotion ? 0 : 300 }}>Adaptive</p>
{/if}
<!-- 11) Performance + style notes -->
<!-- • Built-in transitions use CSS where possible (GPU-friendly transform/opacity) -->
<!-- • Use 'tick:' for JS-driven values (text reveal, counters) — runs each frame -->
<!-- • Avoid transitions on hundreds of items per frame; use 'animate:flip' for reorder + a single batch transition -->
<!-- • Long-duration transitions can confuse e2e tests; pause animations in test mode -->
<!-- 12) Common bugs -->
<!-- • Transitioning a list without key={item.id} → Svelte can't track identity, transitions misfire -->
<!-- • Out transitions skipped on hot reload — restart the dev server -->
<!-- • Crossfade fallback missing → elements disappear instantly when matching pair missing -->
<!-- • Custom tick: that mutates innerHTML — breaks the node for the next transition cycle -->
<!-- • Wrong easing import path ('svelte/easing') vs ('svelte/transition') -->
<!-- • Forgetting prefers-reduced-motion — bad accessibility -->
Why it matters
Reach for built-in transitions (fade, slide, fly, crossfade) and animate:flip for list reorders before any third-party motion library. Provide stable keys, gate durations on prefers-reduced-motion, and write a custom transition with the tick callback only when CSS can’t express the effect.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
<script>import { fade } from 'svelte/transition';</script>
{#if show}<div transition:fade>Hi</div>{/if}
Try it Yourself »
Discussion
Loading…