Automatic, SSR-ready breadcrumbs for SvelteKit via route-level metadata exports. Zero config, fully reactive, server-rendered with top-level await.
Svelte 5 + SvelteKit 2 only. Data layer only — bring your own rendering.
npm install svelte-crumbsThis library relies on Svelte's experimental async compiler option for top-level await in components. This is required.
// svelte.config.js
const config = {
kit: {
experimental: {
remoteFunctions: true
}
},
compilerOptions: {
experimental: {
async: true
}
}
};To also use remote functions in your breadcrumb resolvers, add
kit.experimental.remoteFunctions: trueas well.
<!-- src/routes/products/+page.svelte -->
<script lang="ts" module>
import type { BreadcrumbMeta } from 'svelte-crumbs';
export const breadcrumb: BreadcrumbMeta = async () => ({
label: 'Products'
});
</script><!-- src/routes/+layout.svelte -->
<script lang="ts">
import { createBreadcrumbs } from 'svelte-crumbs';
const getBreadcrumbs = createBreadcrumbs();
const crumbs = $derived(await getBreadcrumbs());
</script>
<nav>
{#each crumbs as crumb, i}
{#if i > 0} / {/if}
<a href={crumb.url}>{crumb.label}</a>
{/each}
</nav>No {#await} blocks needed. Breadcrumbs resolve during SSR and update reactively on client navigation.
<script lang="ts" module>
import type { BreadcrumbMeta } from 'svelte-crumbs';
export const breadcrumb: BreadcrumbMeta = async () => ({
label: 'Settings'
});
</script>The breadcrumb resolver receives the full page object, including page.data. Use +layout.server.ts (not +page.server.ts) so the data is available to child routes' breadcrumbs too:
// src/routes/products/[id]/+layout.server.ts
export async function load({ params }) {
const product = await db.products.find(params.id);
return { product };
}<!-- src/routes/products/[id]/+page.svelte -->
<script lang="ts" module>
import type { BreadcrumbMeta } from 'svelte-crumbs';
export const breadcrumb: BreadcrumbMeta = async (page) => ({
label: page.data.product.name
});
</script>
<script lang="ts">
let { data } = $props();
</script>
<h1>{data.product.name}</h1>Why
+layout.server.ts? Breadcrumb resolvers run for every segment of the URL. When visiting/products/42/edit, the resolver for/products/[id]fires too. If you put the load in+page.server.ts,page.dataon child routes won't haveproduct— layout data cascades down, page data doesn't.
Breadcrumb resolvers can call remote functions that run on the server:
// src/lib/products.remote.ts
import { query } from '$app/server';
export const getProductName = query('unchecked', async (id: string) => {
const product = await db.products.find(id);
return product.name;
});<!-- src/routes/products/[id]/+page.svelte -->
<script lang="ts" module>
import type { BreadcrumbMeta } from 'svelte-crumbs';
import { getProductName } from '$lib/products.remote';
export const breadcrumb: BreadcrumbMeta = async (page) => ({
label: await getProductName(page.params.id ?? '')
});
</script>For dynamic routes that map to known paths:
<script lang="ts" module>
import type { BreadcrumbMeta } from 'svelte-crumbs';
export const breadcrumb: BreadcrumbMeta = {
routes: {
'/docs/getting-started': async () => ({ label: 'Getting Started' }),
'/docs/api-reference': async () => ({ label: 'API Reference' })
}
};
</script><script lang="ts" module>
import type { BreadcrumbMeta } from 'svelte-crumbs';
import HomeIcon from './HomeIcon.svelte';
export const breadcrumb: BreadcrumbMeta = async () => ({
label: 'Home',
icon: HomeIcon
});
</script>Since svelte-crumbs only provides data, you render however you want:
<script lang="ts">
import { createBreadcrumbs } from 'svelte-crumbs';
const getBreadcrumbs = createBreadcrumbs();
const crumbs = $derived(await getBreadcrumbs());
</script>
<ol class="breadcrumb-list">
{#each crumbs as crumb}
<li>
{#if crumb.icon}
{@const Icon = crumb.icon}
<Icon />
{/if}
<a href={crumb.url}>{crumb.label}</a>
</li>
{/each}
</ol>Creates a reactive breadcrumb resolver. Returns a getter function () => Promise<Breadcrumb[]>.
Call createBreadcrumbs() once to set up the reactive state, then use the returned getter inside $derived(await ...) to get breadcrumbs that update on navigation and resolve during SSR.
// What you export from +page.svelte
type BreadcrumbMeta = BreadcrumbResolver | { routes: Record<string, BreadcrumbResolver> };
// Resolver function
type BreadcrumbResolver = (page: Page) => Promise<BreadcrumbData | undefined>;
// Data for one breadcrumb
type BreadcrumbData = { label: string; icon?: Component<any> };
// Resolved breadcrumb with URL
type Breadcrumb = BreadcrumbData & { url: string };buildBreadcrumbMap()— manually build the route-to-resolver mapfilePathToRoute(filePath)— convert glob file path to routematchDynamicRoute(map, route)— match a concrete path against dynamic patternsgetResolversForRoute(map, route)— collect resolvers for a given route path
import.meta.globeagerly imports all+page.sveltefiles at build time- Each file's
breadcrumbexport is collected into aMap<route, resolver> - Route groups like
(app)are stripped from paths - On navigation, the root (
/) resolver is checked first, then each segment is walked from left to right with dynamic[param]matching - Matching resolvers run in parallel, producing the final breadcrumb array
- On SSR, top-level
awaitensures breadcrumbs are rendered in the initial HTML - On the client,
$derivedre-evaluates when the route changes
- Svelte 5 with
compilerOptions.experimental.async: true— uses$derived(await ...)for reactive, SSR-safe breadcrumbs - SvelteKit 2 — relies on
$app/stateandimport.meta.glob - Route groups (
(group)) are stripped from paths
MIT