svelte-crumbs v1.3.0

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 flip can 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 fly transition 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 mounted guard ensures transitions only run client-side.
MIT © 2026 svelte-crumbs (use this however you like)