Where the reader is, spelled out: Jobs › Acme import › Parts. Every crumb with an href navigates through the app's own router link, the current page and any segment without a page of its own read as plain text, and a name that has not loaded yet draws a same-height skeleton instead of an id (breadcrumbs.tsx:43-77). Two shipped shapes ride on presets: speedway's page trail and walmart's topbar crumb, so neither app keeps a private copy.
The canon behind it. docs/design-language/08-navigation-and-shell.md §A7 (08-navigation-and-shell.md:32, a trail earns its row only two or more levels deep) and docs/design-language/09-page-composition.md §A4 (09-page-composition.md:25, breadcrumbs sit first in the fixed composition order, shallow trails hidden).
When to reach for it#
Any page more than one level deep: a record inside a list, a doc inside a section, a settings tab inside an account. The trail earns its row only when it says something the title does not; a single crumb restates the page title, so hideSingle (on by default) renders nothing for it (owner ruling 2026-07-29, carried from speedway/app/components/ShellCrumbs.tsx:154-155).
Do not reach for it to build tabs, a section switcher, or a filter chip row. Crumbs describe where the reader is; they never change what a list shows.
Contract#
items: BreadcrumbItem[](required): each{ key?, label?, href?, pending? }.href: present on every crumb that navigates. Omit it on the current page, and on a segment with no page of its own (/admin/teamsalone is not a page), so it renders as text rather than a link that lands nowhere.pending: the name-bearing segment whose name a route loader has not returned yet; aSkeletonholds its place at crumb height.key: only whenlabelis not a string; falls back tohref, then the index.
presets("trail" | "topbar"): partial-prop bundles, explicit props win.trail:separator="chevron", every crumb quiet,size="xs",hideSingle. Speedway's.shell-crumbs(speedway/app/styles/app.css:259-277) and every playground page header.topbar:separator="slash",emphasizeCurrent,size="sm", never hidden. Walmart's topbarCrumb(walmart-mvp/frontend/src/App.tsx:141-156).
separator("chevron" | "slash", default chevron),emphasizeCurrent(default false),size("xs" | "sm", default xs),hideSingle(default true).linkAs: the app's router link (NextLink, react-routerLink), the sameNavLinkComponentButtonandAppShelltake, so crumbs prefetch and navigate client-side. Without it, a plain<a>.label: the nav's accessible name, default "Breadcrumb". The last text crumb carriesaria-current="page".className: the nav wrapper.
Deriving items from the path#
deriveCrumbs(pathname, rules) builds the items array from a pathname and rules the app declares once, so the walk itself is never hand-rolled (derive-crumbs.ts). Pure and router-free: pass location.pathname from any router.
labels: static segments and their labels; an unlisted segment shows itself capitalized.noPage: segments whose crumb reads as text (nohref), same rule as thehrefbullet above.drop: segments that render no crumb and swallow the one segment after them (speedway hidesworkspacesand the workspace id together; hrefs still advance through both).dynamic: namers for dynamic values keyed by the segment before them (jobsnames the id in/jobs/{id}from what a route loader already knows). Return{ pending: true }while the name loads, ornullto fall back.hide: judge the whole split path;truerenders no trail (speedway's dashboard rule).
The last derived crumb never carries an href: the current page reads as text with aria-current, per the Banned rule below. Extra crumbs a path cannot express (speedway's ?tab= crumb) are appended by the caller after deriving.
Sanctioned combinations#
| Combination | Produces | Where used | Why |
|---|---|---|---|
presets="trail" with linkAs above a PageTitle, in a flex flex-col gap-2 | The house page header | apps/playground/src/app/showcase-title.tsx, apps/playground/src/app/docs/doc-title.tsx | One header shape for every playground and docs page |
presets="trail" with a pending item | A trail whose record name is still loading | speedway's job and team crumbs (ShellCrumbs.tsx:97-104) | The skeleton keeps the row's height; the id never shows |
presets="topbar" with one or two text crumbs | Quiet group, slash, bold current page | walmart's topbar (App.tsx:141-156) | The topbar names the page; it is not a trail, so it is never hidden |
deriveCrumbs feeding presets="trail" | The whole page trail from location.pathname plus declared rules | the playground's "Derived from the path" card (apps/playground/src/app/components/breadcrumbs/gallery.tsx) | The app keeps its vocabulary (labels, page-less segments, namers) and stops rewriting the walk |
Banned combinations#
Do not pass every crumb an href. The habit comes from daisyUI's own breadcrumbs examples, where every item is an anchor; here the current page would link to itself and a page-less group segment would 404, so leave both without one (deriveCrumbs already refuses the last-crumb half). Do not render the trail preset when the app's own topbar already carries the crumb; one location line per page.
Before you adopt this#
Five questions to answer before reaching for Breadcrumbs.
- Does the shell, a parent layout, or a global provider already render this? The app's own topbar may already carry the crumb; one location line per page.
- Does this app already ship a local implementation of the same thing? Not applicable, no local Breadcrumbs clone is documented in either app.
- Does this app's kit pin reach the version this component or prop landed in? Not applicable, no version-landed Breadcrumbs prop is noted in this doc.
- Does the component derive its own accessible name and keyboard path, or must the call site supply them? Kit sets
aria-currenton the last crumb; omithrefthere or it links nowhere. - Which canon §A rules bind this surface, and which does the composition break? §A7 hides a trail under two levels deep; §A4 fixes its position first.