Introduction
Animations can transform a static interface into a dynamic, responsive experience, but they're often overlooked when content is being swapped in and out. Svelte transitions with keys: what developers should know is critical knowledge for building interfaces where page-like content changes feel polished rather than jarring. When you combine Svelte's {#key} block with transitions, you unlock the ability to animate content destruction and creation, giving users visual feedback about what's changing on the page.
In this article, we'll explore how the {#key} directive works, how to pair it with transitions, and when it shines in real-world applications. We'll also cover the gotchas that catch teams when implementing these patterns at scale.
How {#key} recreates content on change
The {#key} block is a reactive control flow directive that destroys and recreates its content whenever the key expression changes. This is fundamentally different from standard reactivity—you're not updating existing DOM nodes; you're tearing down the old subtree and building a new one.
Consider a modal that displays different user profiles. Without {#key}, the modal DOM persists, and only the bound data changes. With {#key}, the entire modal is recreated when the user ID changes, which means form state, scroll position, and focus are all reset by default.
<script>
let userId = 1;
function switchUser(id) {
userId = id;
}
</script>
{#key userId}
<div class="profile">
<h3>User {userId}</h3>
<input type="text" placeholder="Notes" />
</div>
{/key}
<button on:click={() => switchUser(2)}>Next User</button>When you click "Next User," the entire profile div is removed from the DOM and a new one is inserted. The input field is wiped clean because the element was destroyed. This is the behavior we want when swapping distinct pieces of content, and it's where transitions become powerful.
Combine {#key} with transitions
Transitions in Svelte are applied to elements as they enter or leave the DOM. When paired with {#key}, transitions fire on the entire block: the old content fades out, and the new content fades in. This creates a natural, polished swap rather than an instant replacement.
Svelte provides built-in transitions like fade, slide, and scale, or you can write custom transitions using the transition API. The transition: directive applies to any element, including the root of a {#key} block.
<script>
import { fade } from 'svelte/transition';
let userId = 1;
function switchUser(id) {
userId = id;
}
</script>
{#key userId}
<div class="profile" transition:fade={{ duration: 300 }}>
<h3>User {userId}</h3>
<p>Email: user{userId}@example.com</p>
<input type="text" placeholder="Notes" />
</div>
{/key}
<button on:click={() => switchUser(2)}>Next User</button>Now when you switch users, the old profile fades out over 300 milliseconds, and the new one fades in. The transition happens automatically—Svelte detects that the key changed, destroys the old content (triggering its out-transition), and creates the new content (triggering its in-transition).
You can layer multiple transitions by using in: and out: modifiers separately for fine-grained control.
<script>
import { fade, scale } from 'svelte/transition';
let userId = 1;
</script>
{#key userId}
<div
class="profile"
in:scale={{ duration: 250 }}
out:fade={{ duration: 200 }}
>
<h3>User {userId}</h3>
</div>
{/key}Here, the new profile scales in (growing from small to full size), while the old profile fades out. This combination creates more sophisticated visual feedback than a single transition alone.
Great for page-like swaps
The real power of {#key} with transitions emerges in scenarios where you're swapping large content sections—what we might call "page-like" content. This includes modal dialogs, tab panels, multi-step forms, and dashboard sections that should feel like discrete views.
In a real e-commerce dashboard, you might have a tabs component that switches between "Orders," "Returns," and "Analytics." Each tab should reset its internal state and animate in. Without {#key}, form filters and scroll positions would persist across tabs, which is confusing.
<script>
import { slide } from 'svelte/transition';
let activeTab = 'orders';
const tabs = ['orders', 'returns', 'analytics'];
function switchTab(tab) {
activeTab = tab;
}
</script>
<div class="tabs">
{#each tabs as tab}
<button
class:active={activeTab === tab}
on:click={() => switchTab(tab)}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
{/each}
</div>
{#key activeTab}
<div class="tab-content" transition:slide={{ duration: 250 }}>
{#if activeTab === 'orders'}
<OrdersPanel />
{:else if activeTab === 'returns'}
<ReturnsPanel />
{:else}
<AnalyticsPanel />
{/if}
</div>
{/key}Each time you switch tabs, the content slides out and the new tab's content slides in. More importantly, each panel component remounts fresh—its internal state (filters, selections, scroll) resets. This is the expected behavior in most tab interfaces.
This pattern also works well for wizard-style flows or modal sequences where you're displaying different screens based on application state. The transition provides a bridge between states that makes the transition feel intentional.
Keep animations short
While transitions are powerful, one of the most common mistakes is making them too long. A 1-second fade-out followed by a 1-second fade-in wastes 2 seconds of the user's time on every navigation. At scale, this adds up.
We've found that durations between 200 and 400 milliseconds work well in most applications. This range is fast enough to feel responsive but slow enough that users perceive the animation and understand something has changed.
// Good: snappy but perceptible
transition:fade={{ duration: 250 }}
// Avoid: too slow, feels sluggish
transition:fade={{ duration: 1000 }}
// Avoid: too fast, users might miss it
transition:fade={{ duration: 50 }}Also consider the context. A modal appearing for the first time might warrant a 300ms entrance. But dismissing an empty dialog? 150ms is plenty. Match animation duration to the significance of the content change.
Another consideration: if you're animating between states where users frequently switch back and forth (like tabs or a carousel), keep durations on the lower end. If it's a one-time flow like a checkout wizard, slightly longer transitions feel less repetitive.
Common mistakes
Mistake 1: Using {#key} on the wrong scope. It's tempting to wrap entire pages in {#key}, but this recreates more than you need. If only the main content should change, scope the key block to that section, not the entire layout with header and sidebar.
// Avoid: recreates header, sidebar, footer
{#key currentPage}
<Header />
<Sidebar />
<MainContent />
<Footer />
{/key}
// Better: recreate only what changes
<Header />
<Sidebar />
{#key currentPage}
<MainContent />
{/key}
<Footer />Mistake 2: Forgetting that {#key} resets component state. If you have form inputs or internal component state inside a {#key} block, they're destroyed on key change. This is usually intentional, but it can surprise you if you expect state to persist.
Mistake 3: Applying transitions at the wrong level. The transition needs to be on an element inside the {#key} block, not on the block itself. Svelte's compiler is smart, but being explicit helps.
// Correct: transition on the element inside {#key}
{#key userId}
<div transition:fade>Content</div>
{/key}
// Still works, but less clear
{#key userId}
<div>
<section transition:fade>Content</section>
</div>
{/key}Mistake 4: Over-animating everything. Not every state change needs a transition. Rapid updates (like filtering results as the user types) often feel better without transitions. Reserve animations for intentional, discrete state changes.
Key takeaways
- The
{#key}directive destroys and recreates DOM content when its key expression changes, resetting all internal state. - Combine
{#key}with transitions (fade, slide, scale) to animate content swaps smoothly instead of instantly. - Use this pattern for page-like content swaps: tabs, modals, wizard steps, and dashboard sections that should feel like distinct views.
- Keep transition durations between 200–400 milliseconds in most cases. Faster animations feel snappy; slower ones feel sluggish.
- Scope
{#key}blocks carefully to avoid recreating more of the DOM tree than necessary. - Remember that
{#key}recreation is intentional—form state and component state inside the block will be wiped clean. - Use separate
in:andout:transitions for more sophisticated effects, but don't over-complicate for the sake of animation.






Comments
Loading comments…