Animated breadcrumbs

Render the crumbs array with Svelte's built-in crossfade, fly and flip. This is what drives the breadcrumb bar above — toggle "Animate breadcrumbs" in the sidebar to watch it.

Full example

Crumbs sit in a CSS grid, one column each, so flip can reposition the ones that stay while crossfade pairs those entering and leaving. A staggered fly fallback covers crumbs with no counterpart.

<script lang="ts">
  import { crossfade, fly } from 'svelte/transition';
  import { flip } from 'svelte/animate';
  import { onMount } from 'svelte';
  import { getCrumbs } from 'svelte-crumbs';

  const crumbs = $derived(await getCrumbs());

  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>

How it works

  • Grid layout — each crumb is pinned to a column, so flip animates position changes as crumbs come and go.
  • Crossfade — a crumb in both the old and new trail morphs in place. Unmatched crumbs fall back to fly.
  • Staggered timing — outgoing crumbs leave last-first, then incoming crumbs arrive in order. The result cascades.
  • SSR safe — the mounted guard keeps transitions client-side.