A static UI works. A delightful UI moves. Animation and theming are what give apps personality — the fade-in of a modal, the spring of a draggable element, the instant mood switch from light to dark mode.
Svelte makes this surprisingly easy with built-in transitions, motion utilities, and flexible theming strategies. No external animation library required.
In this article, you’ll learn how to:
Add transitions like fade, fly, and scale.
Animate lists when items move with animate:flip.
Use motion stores (tweened, spring) for reactive animations.
Build theming systems with CSS variables or props.
By the end, you’ll have the tools to make your UI not just functional, but alive.
Making Components Come Alive with Transitions ✨
A static UI is fine… but a delightful UI breathes. Svelte makes it almost too easy to add motion with the transition:
directive.
Example: Fade Transition
src/lib/FadeDemo.svelte
<script>
// Import one of Svelte's built-in transitions
import { fade } from 'svelte/transition';
// Controls whether the box is visible
let visible = true;
</script>
<!-- Button toggles the state -->
<button on:click={() => visible = !visible}>
Toggle Box
</button>
<!--
- The {#if} block ensures the <div> is actually added/removed from the DOM
- transition:fade makes it fade in/out when that happens
-->
{#if visible}
<div transition:fade>✨ Hello World</div>
{/if}
Using the Component
src/routes/+page.svelte
<script>
import FadeDemo from '$lib/FadeDemo.svelte';
</script>
<h1>Transitions Demo</h1>
<FadeDemo />
What’s Happening
- When
visible
istrue
, Svelte creates the<div>
and mounts it into the DOM.
- Because we used
transition:fade
, it fades in smoothly.
When
visible
becomesfalse
, Svelte runs the fade-out animation before removing the element from the DOM.The
{#if}
block is essential:
- If you just hid the
<div>
with CSS (display: none
), there would be no mount/unmount → no transition.
- All of this happens with zero manual CSS keyframes or JavaScript timers.
✅ That’s literally one line (transition:fade
) to get buttery-smooth animations.
Customizing Transitions ✨
Svelte ships with several built-in transitions: fade
, fly
, slide
, scale
, and blur
.
Each of them accepts optional parameters like duration
, delay
, or effect-specific settings.
Example: Different Transitions
src/lib/TransitionsDemo.svelte
<script>
import { fade, fly, scale } from 'svelte/transition';
let show = true;
</script>
<button on:click={() => show = !show}>
Toggle Elements
</button>
{#if show}
<div transition:fade={{ duration: 2000 }}>
🌙 Slow Fade (2s)
</div>
<div transition:fly={{ y: 100, duration: 700 }}>
🚀 Fly in from below
</div>
<div transition:scale={{ start: 0.5, duration: 600 }}>
🔍 Scaling in
</div>
{/if}
Using It in the Page
src/routes/+page.svelte
<script>
import TransitionsDemo from '$lib/TransitionsDemo.svelte';
</script>
<h1>Transitions Demo</h1>
<TransitionsDemo />
How It Works
-
fade={{ duration: 2000 }}
→ element fades in/out over 2 seconds. -
fly={{ y: 100, duration: 700 }}
→ element starts 100px lower and flies upward into place over half a second. -
scale={{ start: 0.5 }}
→ element begins at 50% size and scales up to full size.
👉 You can combine transitions with conditional rendering ({#if}
) to animate elements as they enter or leave the DOM.
Animate Lists with animate:flip
Lists often need animations when items are added, removed, or reordered. Without animation, the DOM just snaps into place — but with animate:flip
, Svelte smoothly transitions items from their old position to their new position.
FLIP stands for:
- First → record the element’s initial position.
- Last → record its final position after state changes.
- Invert → figure out the delta between them.
- Play → animate between the two.
Example: Animated List
src/lib/AnimatedList.svelte
<script>
import { flip } from 'svelte/animate';
let items = [1, 2, 3, 4, 5];
function shuffle() {
// Shuffle the array randomly
items = [...items].sort(() => Math.random() - 0.5);
}
</script>
<button on:click={shuffle}>Shuffle</button>
<ul>
{#each items as item (item)}
<!-- (item) makes it a keyed list -->
<li animate:flip>{item}</li>
{/each}
</ul>
<style>
ul {
display: flex;
gap: 1rem;
list-style: none;
padding: 0;
}
li {
background: lightblue;
padding: 1rem;
border-radius: 6px;
min-width: 40px;
text-align: center;
}
</style>
Using It
src/routes/+page.svelte
<script>
import AnimatedList from '$lib/AnimatedList.svelte';
</script>
<h1>Animated List Demo</h1>
<AnimatedList />
Why It Works
-
{#each items as item (item)}
→ makes it a keyed list using the item value as the key. -
animate:flip
→ tells Svelte: “when items move, animate from their old position to their new one.” - When you press Shuffle, Svelte compares before/after positions and smoothly animates the changes.
👉 Without (item)
as the key, Svelte might just reuse DOM nodes, and the animation wouldn’t behave as expected.
Motion Utilities — tweened
and spring
Not all motion is about elements entering and leaving the DOM. Sometimes you want the values themselves to animate smoothly — numbers ticking up, positions gliding across the screen, sliders snapping into place.
Svelte ships two special reactive stores for this:
-
tweened
→ animates values linearly or with easing. -
spring
→ animates values with a natural spring-like effect.
Example 1: Tweened Store (Smooth Number Counting)
src/lib/TweenedCounter.svelte
<script>
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
// Create a tweened store with starting value 0
let count = tweened(0, { duration: 400, easing: cubicOut });
function increment() {
// .set() updates the store
// $count is the "auto-subscription" value of the store
count.set($count + 10);
}
</script>
<button on:click={increment}>Add 10</button>
<p>Current count: {$count.toFixed(0)}</p>
👉 The magic here is $count
: whenever you prefix a store with $
, Svelte auto-subscribes to it and gives you its current value. When you call count.set(...)
, it doesn’t just jump — it smoothly animates toward the new value.
Example 2: Spring Store (Smooth Position Animation)
src/lib/SpringBall.svelte
<script>
import { spring } from 'svelte/motion';
// Create a spring store with an object value
let pos = spring({ x: 0, y: 0 });
function moveRight() {
pos.update(p => ({ ...p, x: p.x + 100 }));
}
</script>
<button on:click={moveRight}>Move Right</button>
<div class="ball" style="transform: translate({$pos.x}px, {$pos.y}px)">
🟢
</div>
<style>
.ball {
position: relative;
display: inline-block;
margin-top: 1rem;
font-size: 2rem;
transition: transform 0.1s linear;
}
</style>
Now when you click the button, the green dot slides right — not instantly, but with a springy “bounce.”
This feels more natural than a plain tween and is great for draggable UIs, sliders, or playful micro-interactions.
Using Them in Your Page
src/routes/+page.svelte
<script>
import TweenedCounter from '$lib/TweenedCounter.svelte';
import SpringBall from '$lib/SpringBall.svelte';
</script>
<h1>Motion Demos</h1>
<TweenedCounter />
<SpringBall />
Theming Strategies
Sooner or later, every app needs theming: light/dark mode, brand palettes, or even seasonal tweaks like “holiday colors.” In Svelte you have multiple ways to achieve this — let’s look at the most common.
Option 1: CSS Variables (Global Theme)
Use CSS custom properties to define colors at the root level. Toggle a class on <body>
(or <html>
) to switch themes.
src/routes/+page.svelte
<script>
function toggleTheme() {
document.body.classList.toggle("dark");
}
</script>
<button on:click={toggleTheme}>Toggle Theme</button>
<div class="box">Hello Theming</div>
<style>
/* Define light theme variables on :root */
:global(:root) {
--bg: white;
--text: black;
}
/* Override them when .dark is present */
:global(.dark) {
--bg: black;
--text: white;
}
.box {
background: var(--bg);
color: var(--text);
padding: 1rem;
border-radius: 6px;
}
</style>
✅ Works app-wide, good for shared themes.
Option 2: Prop-Driven Themes (Per Component)
Sometimes you want a component to be themed independently of the global setting. In that case, pass a theme
prop.
src/lib/ThemedBox.svelte
<script>
export let theme = "light"; // "light" | "dark"
</script>
<div class:dark={theme === "dark"} class="box">
Themed Component ({theme})
</div>
<style>
.box {
padding: 1rem;
border-radius: 6px;
}
.dark {
background: black;
color: white;
}
:global(.box):not(.dark) {
background: white;
color: black;
}
</style>
Usage in src/routes/+page.svelte
<script>
import ThemedBox from '$lib/ThemedBox.svelte';
</script>
<ThemedBox theme="light" />
<ThemedBox theme="dark" />
✅ Good for isolated widgets where theme varies per instance.
Option 3: Tailwind for Theming (Utility-First)
Tailwind makes theming effortless with its dark:
variant.
src/routes/+page.svelte
<script>
function toggleTheme() {
document.body.classList.toggle("dark");
}
</script>
<button on:click={toggleTheme}>Toggle Theme</button>
<div class="p-4 rounded-lg bg-white text-black dark:bg-black dark:text-white">
Hello Tailwind Dark Mode
</div>
Common Gotchas ⚠️
Even though Svelte’s styling and motion features are powerful, there are a few traps you’ll want to avoid:
1. Transitions only work when elements enter/leave the DOM
This won’t work (no transition):
<!-- ❌ Just hiding with CSS -->
<div style="display: {visible ? 'block' : 'none'}" transition:fade>
I won’t fade properly
</div>
Why? The <div>
never actually leaves the DOM — it’s just hidden.
✅ Correct way:
{#if visible}
<div transition:fade>I fade in and out!</div>
{/if}
2. Always key your lists when animating with flip
Without keys:
<ul>
{#each items as item}
<li animate:flip>{item}</li>
{/each}
</ul>
❌ Items can jump or morph strangely if order changes.
✅ With keys:
<ul>
{#each items as item (item)}
<li animate:flip>{item}</li>
{/each}
</ul>
Now Svelte knows exactly which element corresponds to which item.
3. Don’t mix too many motion types
It’s tempting to pile on transitions and animations:
<div transition:fade transition:scale animate:flip>
Chaos 😵
</div>
While Svelte lets you do this, the result is often distracting.
👉 Rule of thumb: pick one or two complementary effects (like fade
+ slide
), and keep it purposeful.
Quick Recap
In this chunk, you learned how to:
- Use Svelte’s built-in
transition:
directive. - Animate reordering with
animate:flip
. - Use motion stores (
tweened
andspring
). - Implement theming via CSS variables or props.
You now know how to bring your Svelte components to life — with smooth transitions, animated lists, reactive motion stores, and multiple theming strategies. These techniques add polish and personality to your apps without unnecessary complexity.
👉 In the final article of this series, we’ll bring in Tailwind CSS — combining Svelte’s scoped styles with utility-first classes for maximum productivity and design consistency.
Next article: Tailwind + Svelte (Utility-First Styling at Scale)
Follow me on DEV for future posts in this deep-dive series.
https://dev.to/a1guy
If it helped, leave a reaction (heart / bookmark) — it keeps me motivated to create more content
Checkout my offering on YouTube with (growing) crash courses and content on JavaScript, React, TypeScript, Rust, WebAssembly, AI Prompt Engineering and more: @LearnAwesome
Top comments (0)