Agent docs

Breadcrumbs

Where the reader is, spelled out; the trail above a page and the topbar crumb.

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/teams alone 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; a Skeleton holds its place at crumb height.
    • key: only when label is not a string; falls back to href, 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 topbar Crumb (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 (Next Link, react-router Link), the same NavLinkComponent Button and AppShell take, so crumbs prefetch and navigate client-side. Without it, a plain <a>.
  • label: the nav's accessible name, default "Breadcrumb". The last text crumb carries aria-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 (no href), same rule as the href bullet above.
  • drop: segments that render no crumb and swallow the one segment after them (speedway hides workspaces and the workspace id together; hrefs still advance through both).
  • dynamic: namers for dynamic values keyed by the segment before them (jobs names the id in /jobs/{id} from what a route loader already knows). Return { pending: true } while the name loads, or null to fall back.
  • hide: judge the whole split path; true renders 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#

CombinationProducesWhere usedWhy
presets="trail" with linkAs above a PageTitle, in a flex flex-col gap-2The house page headerapps/playground/src/app/showcase-title.tsx, apps/playground/src/app/docs/doc-title.tsxOne header shape for every playground and docs page
presets="trail" with a pending itemA trail whose record name is still loadingspeedway'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 crumbsQuiet group, slash, bold current pagewalmart'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 rulesthe 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.

  1. 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.
  2. Does this app already ship a local implementation of the same thing? Not applicable, no local Breadcrumbs clone is documented in either app.
  3. 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.
  4. Does the component derive its own accessible name and keyboard path, or must the call site supply them? Kit sets aria-current on the last crumb; omit href there or it links nowhere.
  5. 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.
@versable-git/ui · reference, canon, and method, read in place