Introduction
Svelte ships with motion as a first-class citizen, and when you need transitions and animations: patterns, pitfalls and fixes, you have powerful primitives at your fingertips. Motion can delight users, guide attention, and signal state changes—but it can also distract, frustrate, or break accessibility if applied carelessly. In this article, we'll walk through the practical patterns we've seen work across production e-commerce and SaaS apps, the common pitfalls that catch teams mid-sprint, and the fixes that keep motion feeling intentional.
Built-in transitions: fade, fly, slide and scale
Svelte's transition directive handles the most common motion needs: when an element enters or leaves the DOM, the transition smoothly interpolates between invisible and visible, or from one position to another.
The four most useful built-in transitions are:
- fade: opacity only, the subtlest option
- fly: combines opacity with movement (x or y offset), often used for overlays
- slide: height-based collapse or expand, perfect for toggled sections
- scale: transforms scale from a point, good for modals or emphasis
Here's a cart notification that fades in and out:
<script>
import { fade } from 'svelte/transition';
let notificationVisible = false;
function addToCart() {
notificationVisible = true;
setTimeout(() => { notificationVisible = false; }, 3000);
}
</script>
{#if notificationVisible}
<div transition:fade={{ duration: 300 }}>
✓ Added to cart
</div>
{/if}
<button on:click={addToCart}>Add to cart</button>For a modal that should feel snappier, use fly with direction:
<script>
import { fly } from 'svelte/transition';
let modalOpen = false;
</script>
{#if modalOpen}
<div transition:fly={{ y: 20, duration: 200 }}>
<h2>Confirm order</h2>
<p>Your total is $49.99</p>
<button on:click={() => (modalOpen = false)}>Close</button>
</div>
{/if>When collapsing a form section, slide is more natural than fade because it respects the height of content:
<script>
import { slide } from 'svelte/transition';
let showShipping = false;
</script>
{#if showShipping}
<div transition:slide={{ duration: 250 }}>
<label>Shipping address</label>
<input type="text" placeholder="Street" />
</div>
{/if}
<button on:click={() => (showShipping = !showShipping)}>
{showShipping ? 'Hide' : 'Show'} shipping
</button>Each transition accepts duration (milliseconds) and optional easing. We find duration: 200–300ms feels responsive for most UI; anything faster feels jarring, anything slower feels sluggish on desktop.
Different enter and exit effects with in: and out:
Sometimes you want different motion on entry than on exit. That's where the in: and out: directives shine. A modal might fly in from above but fade out, or a toast might slide in but scale down on exit.
<script>
import { fly, fade } from 'svelte/transition';
let toastVisible = false;
function showToast() {
toastVisible = true;
setTimeout(() => { toastVisible = false; }, 4000);
}
</script>
{#if toastVisible}
<div
in:fly={{ y: -50, duration: 300 }}
out:fade={{ duration: 200 }}
>
Order placed! Check your email.
</div>
{/if}
<button on:click={showToast}>Place order</button>In e-commerce checkout flows, we've used in:fly paired with out:fade to make error messages feel urgent on arrival but gentle on dismissal. It's a small detail, but users notice the asymmetry and it reinforces the hierarchy.
You can also chain multiple transitions by using the pipe syntax in your CSS or by composing custom transition functions, but for most cases, picking one in: and one out: is cleaner and performs better.
Reordering lists with animate:flip
Animating list reordering is tricky. If you sort or filter an array and simply re-render, items snap into place. The animate:flip directive uses FLIP (First, Last, Invert, Play) to smoothly morph elements from their old positions to new ones.
<script>
import { flip } from 'svelte/animate';
let todos = [
{ id: 1, text: 'Ship feature', done: false },
{ id: 2, text: 'Review PR', done: false },
{ id: 3, text: 'Update docs', done: false }
];
function toggleTodo(id) {
const todo = todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
todos = todos.sort((a, b) => (a.done ? 1 : -1));
}
</script>
<ul>
{#each todos as todo (todo.id)}
<li animate:flip={{ duration: 200 }}>
<input
type="checkbox"
checked={todo.done}
on:change={() => toggleTodo(todo.id)}
/>
<span class:done={todo.done}>{todo.text}</span>
</li>
{/each}
</ul>Without animate:flip, checking a todo would cause the list to instantly reorganize. With it, the checked item glides to the bottom while others shift smoothly upward. It's especially valuable in data tables, kanban boards, and search results where order changes frequently.
One note: animate:flip only handles position; if you want color or scale changes too, layer a transition: directive alongside it.
Respecting reduced motion preferences
Motion accessibility is non-negotiable. Some users have vestibular disorders, migraines, or simply prefer less visual noise. The CSS media query prefers-reduced-motion lets us detect this preference and disable or drastically reduce animation.
<script>
import { fly, fade } from 'svelte/transition';
let prefersReducedMotion = false;
onMount(() => {
prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
});
</script>
{#if modalOpen}
<div
transition:fly={{
y: prefersReducedMotion ? 0 : 20,
duration: prefersReducedMotion ? 0 : 200
}}
>
<h2>Confirm order</h2>
</div>
{/if}A cleaner approach is to create a store that tracks the preference once and reuse it across components:
// src/lib/motion.ts
import { writable } from 'svelte/store';
function createMotionStore() {
const { subscribe, set } = writable(false);
if (typeof window !== 'undefined') {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
set(mediaQuery.matches);
mediaQuery.addEventListener('change', (e) => set(e.matches));
}
return { subscribe };
}
export const prefersReducedMotion = createMotionStore();Then in any component:
<script>
import { prefersReducedMotion } from '$lib/motion';
import { fly } from 'svelte/transition';
</script>
{#if toastOpen}
<div
transition:fly={{
y: $prefersReducedMotion ? 0 : -50,
duration: $prefersReducedMotion ? 0 : 300
}}
>
Success!
</div>
{/if}Setting duration: 0 when reduced motion is preferred effectively disables the animation while keeping the logic intact. This respects user preferences without requiring conditional rendering of entire UI branches.
Common mistakes
- Using transitions on every element. Not every state change needs motion. Overuse creates noise and slows perception. Reserve transitions for user feedback, state clarity, and guided attention.
- Ignoring duration and easing. A 1-second fade on a notification feels sluggish. A 50ms transition on a toggle feels broken. Test on both desktop and mobile; slower networks make long animations feel janky.
- Combining transition: with in: or out: by accident. If you use
transition:, you don't needin:orout:—they override each other and confusion results. Pick one approach per element. - Forgetting to track the DOM key in each blocks. When animating list reordering, always provide a stable key to the
{#each}. Without it,animate:flipgets confused and animates the wrong elements. - Not testing with reduced motion enabled. Open your OS accessibility settings, enable "reduce motion," and refresh. If animations still run, your code isn't respecting user preferences. This can cause real harm to users with vestibular disorders.
- Performance drag from overly complex custom transitions. Custom transitions are powerful, but if you're doing expensive DOM calculations inside a transition function, it will stutter on lower-end devices. Keep transition logic lean.
Key takeaways
- Svelte's built-in transitions (
fade,fly,slide,scale) cover 90% of real-world use cases; start there before writing custom transitions. - Use
in:andout:when entry and exit motion differ; it creates more nuanced, intentional interactions. animate:fliptransforms jarring list reordering into smooth, delightful motion—always use it for sorted or filtered lists.- Check
prefers-reduced-motionon mount and respect it by zeroing duration or disabling animation entirely; accessibility isn't optional. - Keep animations under 300ms on desktop; anything longer feels sluggish. Test on real devices and network conditions before shipping.
- Motion should clarify state or guide attention, never distract. If you're unsure whether an animation adds value, it probably doesn't.






Comments
Loading comments…