Animations
Svelte’s animate: directive smoothly animates LAYOUT changes — reorders, inserts, deletions — using FLIP. Combined with tweened/spring stores and CSS transitions you get production-quality motion in tiny diffs.
flip, tweened, spring, motion patterns
EXAMPLE
<script>
import { flip } from 'svelte/animate';
import { fade, fly, slide } from 'svelte/transition';
import { tweened, spring } from 'svelte/motion';
import { cubicOut, elasticOut } from 'svelte/easing';
let items = [
{ id: 1, text: 'apple' },
{ id: 2, text: 'banana' },
{ id: 3, text: 'cherry' },
];
function shuffle() {
items = [...items].sort(() => Math.random() - 0.5);
}
function remove(id) {
items = items.filter((i) => i.id !== id);
}
function add() {
items = [...items, { id: Date.now(), text: 'new ' + items.length }];
}
</script>
<!-- 1) animate:flip — animates LAYOUT changes -->
<button on:click={shuffle}>Shuffle</button>
<button on:click={add}>Add</button>
<ul>
{#each items as item (item.id)}
<li animate:flip={{ duration: 300, easing: cubicOut }}>
{item.text}
<button on:click={() => remove(item.id)}>×</button>
</li>
{/each}
</ul>
<!-- FLIP = First, Last, Invert, Play — Svelte calculates the old/new positions and tweens. -->
<!-- 2) Combine FLIP with transitions on enter/leave -->
<ul>
{#each items as item (item.id)}
<li
in:fly={{ y: -10, duration: 200 }}
out:fade={{ duration: 150 }}
animate:flip={{ duration: 300 }}
>{item.text}</li>
{/each}
</ul>
<!-- 3) tweened store — animate a numeric value -->
<script>
import { tweened } from 'svelte/motion';
const progress = tweened(0, { duration: 400, easing: cubicOut });
</script>
<button on:click={() => progress.set(Math.random())}>New target</button>
<progress value={$progress}></progress>
<p>{($progress * 100).toFixed(0)}%</p>
<!-- 4) Tweening a vector — multiple values together -->
<script>
const pos = tweened({ x: 0, y: 0 }, { duration: 300 });
function moveTo(x, y) { pos.set({ x, y }); }
</script>
<div style="transform: translate({$pos.x}px, {$pos.y}px)">Box</div>
<!-- 5) spring store — physics-based motion -->
<script>
const spr = spring(0, { stiffness: 0.1, damping: 0.4, precision: 0.001 });
function nudge() { spr.set($spr + 30); }
</script>
<div style="transform: translateX({$spr}px)">Springs</div>
<!-- spring overshoots + settles; great for UI gestures. -->
<!-- 6) Animated SVG path -->
<script>
import { writable } from 'svelte/store';
import { tweened } from 'svelte/motion';
const d = tweened(0, { duration: 800, easing: cubicOut });
let width = 100;
</script>
<svg viewBox="0 0 200 100">
<rect x="0" y="40" width={$d} height="20" fill="steelblue" />
</svg>
<button on:click={() => d.set(width)}>Fill</button>
<button on:click={() => d.set(0)}>Empty</button>
<!-- 7) Coordinating transitions with crossfade -->
<script>
import { crossfade } from 'svelte/transition';
const [send, receive] = crossfade({
duration: 200,
fallback: (node) => ({ duration: 150, css: (t) => `opacity: ${t}` }),
});
let left = [ { id:1, text:'a' }, { id:2, text:'b' } ];
let right = [];
function move(item, from, to) {
from.splice(from.indexOf(item), 1); to.push(item);
left = left; right = right;
}
</script>
<ul>{#each left as i (i.id)}<li in:receive={{ key: i.id }} out:send={{ key: i.id }} on:click={() => move(i, left, right)}>{i.text}</li>{/each}</ul>
<ul>{#each right as i (i.id)}<li in:receive={{ key: i.id }} out:send={{ key: i.id }} on:click={() => move(i, right, left)}>{i.text}</li>{/each}</ul>
<!-- 8) Custom animation function -->
<script>
function whoosh(node, { duration = 400 } = {}) {
return {
duration,
css: (t) => `transform: scale(${0.95 + 0.05 * t}) translateY(${(1-t) * -10}px); opacity: ${t}`,
};
}
</script>
{#if visible}
<div in:whoosh>Custom enter</div>
{/if}
<!-- 9) Sequence with delay -->
{#each items as item, i (item.id)}
<div in:fly={{ y: 20, delay: i * 80 }}>{item.text}</div>
{/each}
<!-- 10) Performance + UX -->
<!-- • All built-in transitions use CSS where possible (compositing-only, GPU-friendly) -->
<!-- • Use shallowRef-style data (assign new array, don't mutate) so Svelte detects changes -->
<!-- • Animate transform/opacity, not width/height — better FPS -->
<!-- • Respect prefers-reduced-motion -->
<script>
import { onMount } from 'svelte';
let reduceMotion = false;
onMount(() => {
reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
});
</script>
{#if visible}
<p transition:fade={{ duration: reduceMotion ? 0 : 300 }}>Adaptive</p>
{/if}
<!-- 11) Common bugs -->
<!-- • Items reorder but no animate:flip — list snaps -->
<!-- • Forgot keys in {#each} — FLIP can't track identity, animations misfire -->
<!-- • Transitioning width / height in tweened — layout thrash; use transform/scale -->
<!-- • Springs run forever — set 'precision' to stop near target -->
<!-- • prefers-reduced-motion ignored — bad UX for users with vestibular sensitivities -->
<!-- • crossfade fallback missing → elements vanish instantly when no matching pair -->
<!-- • Animation library + custom store both mutating dom node — racing -->
Why it matters
Svelte’s motion primitives (animate:flip, tweened, spring, crossfade) cover most UI animation without any third-party library. Combine FLIP for reorder with in:/out: transitions for enter/leave, animate transform + opacity only, and honour prefers-reduced-motion so vestibular-sensitive users aren’t left out.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
<script>import { flip } from 'svelte/animate';</script>
{#each items as i (i.id)}<div animate:flip>{i.name}</div>{/each}
Try it Yourself »
Discussion
Loading…