Animated breadcrumbs
svelte-crumbs is headless — it gives you a reactive crumbs array and leaves rendering
entirely up to you. Below is a suggested approach for rendering breadcrumbs with smooth
transitions using Svelte's built-in crossfade, fly, and flip.
This is exactly what powers the breadcrumb bar in this demo site — toggle "Animate breadcrumbs"
in the sidebar to see it in action.
Full example
The key idea is to render crumbs in a CSS grid (one column per crumb) so that Svelte's flip animation can smoothly
reposition crumbs that stay visible, while crossfade pairs entering and
leaving crumbs. A staggered fly fallback handles crumbs that
have no matching counterpart.
<script module lang="ts">
<script lang="ts">
import { crossfade, fly } from 'svelte/transition';
import { flip } from 'svelte/animate';
import { onMount } from 'svelte';
import { createBreadcrumbs } from 'svelte-crumbs';
const getBreadcrumbs = createBreadcrumbs();
const crumbs = $derived(await getBreadcrumbs());
let mounted = $state(false);
onMount(() => { mounted = true; });
let count = $derived(crumbs.length);
let prevCount = $derived(crumbs.length);
$effect.pre(() => {
prevCount = count;
count = crumbs.length;
});
const DURATION = 200;
const STAGGER = 60;
const [send, receive] = crossfade({
duration: 200,
fallback(node, _params, intro) {
if (!mounted) return { duration: 0 };
const i = parseInt(node.getAttribute('data-i') ?? '0');
const outTotal = (prevCount - 1) * STAGGER + DURATION;
const delay = intro
? outTotal + i * STAGGER
: Math.max(0, prevCount - 1 - i) * STAGGER;
return fly(node, intro
? { x: -4, duration: DURATION, delay }
: { y: 4, duration: DURATION, delay });
}
});
</script>
<nav
aria-label="Breadcrumbs"
class="grid auto-cols-auto items-center gap-2 text-sm"
>
{#each crumbs as crumb, i (crumb.url)}
<span
class="inline-flex items-center gap-2"
data-i={i}
style:grid-column={i + 1}
style:grid-row="1"
in:receive={{ key: crumb.url }}
out:send={{ key: crumb.url }}
animate:flip={{ duration: 150, delay: 150 }}
>
{#if i > 0}
<span aria-hidden="true">/</span>
{/if}
{#if i < crumbs.length - 1 || crumbs.length === 1}
<a href={crumb.url}>{crumb.label}</a>
{:else}
<span aria-current="page">{crumb.label}</span>
{/if}
</span>
{/each}
</nav>
</script>How it works
- Grid layout — each crumb is pinned to a grid column, so
flipcan animate position changes when crumbs are added or removed. - Crossfade — when a crumb exists in both the old and new trail, crossfade
morphs it in place. For crumbs without a match, the fallback
flytransition kicks in. - Staggered timing — outgoing crumbs fly out with a reverse stagger (last crumb first), then incoming crumbs fly in sequentially. This creates a smooth cascading effect.
- SSR safe — the
mountedguard ensures transitions only run client-side.