iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…