Svelte Motion & Theming Guide: Transitions, Animations, and Dark Mode Explained



This content originally appeared on DEV Community and was authored by Ali Aslam

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

  1. When visible is true, Svelte creates the <div> and mounts it into the DOM.
  • Because we used transition:fade, it fades in smoothly.
  1. When visible becomes false, Svelte runs the fade-out animation before removing the element from the DOM.

  2. The {#if} block is essential:

  • If you just hid the <div> with CSS (display: none), there would be no mount/unmount → no transition.
  1. 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

  1. {#each items as item (item)} → makes it a keyed list using the item value as the key.
  2. animate:flip → tells Svelte: “when items move, animate from their old position to their new one.”
  3. 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 and spring).
  • 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


This content originally appeared on DEV Community and was authored by Ali Aslam