Breadcrumb
Shows the current page's place in the hierarchy with links back to its ancestors, and folds a deep trail to fit — into a menu of hidden ancestors or a single back-link.
@garn/ui/breadcrumbOn this page
Default
Home / Components / current page — the manual trail with per-gap separators.
Collapsing long trails
Collapsed
A manual BreadcrumbEllipsis stands in for hidden middle crumbs (sr-only "More").
Auto collapse
Static `maxItems`: fold the middle behind an expand-in-place "…" button; click to reveal all.
Responsive
Width-aware `collapse="responsive"`: drag the container narrow and middle crumbs fold into a "…" menu of hidden ancestors; widen to restore them.
Back to parent
Mobile `collapse="back"`: below the fit width the whole trail becomes a single "‹ Parent" back-link; widen to restore.
Crumb content
Leading icon
Per-crumb `leading` icons: an icon-only home root (named with aria-label) plus icon-decorated crumbs.
Custom separator
A custom `separator` node (a slash) replaces the chevron the collapse mode inserts; it stays decorative.
Composition
Header compose
The app-shell contract: a bounded, w-full flex slot lets the responsive breadcrumb measure and fold beside other header content.
As child
BreadcrumbLink `asChild` wrapping a router Link (shown with a plain anchor) for client navigation.
Internationalization
Rtl
dir="rtl" mirrors the trail (root on the right), flips the default chevron, and opens the collapse menu on the mirrored side.
Related components
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add breadcrumb- Registry
breadcrumb- Deps
@radix-ui/react-slotlucide-react- Registry deps
utilsfocus-ringuse-resize-observeruse-merged-refuse-overflow-collapsedropdown-menu
Import
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis } from "@garn/ui/breadcrumb";Anatomy
The parts this component is built from — compose them to assemble it.
BreadcrumbRequired[data-slot="breadcrumb"]The <nav aria-label="breadcrumb"> landmark. Forwards className, so it drops into a header as a bounded flex child.
BreadcrumbListRequired[data-slot="breadcrumb-list"]The ordered trail. Manual by default; maxItems folds the middle behind an expand-in-place "…" button; collapse="responsive" folds it into a width-aware menu of hidden ancestors; collapse="back" swaps a too-narrow trail for a single parent back-link. Every collapse mode inserts separators for you (pass items only).
BreadcrumbItemRequired[data-slot="breadcrumb-item"]One crumb (a list item).
BreadcrumbLink[data-slot="breadcrumb-link"]A link to an ancestor; asChild to wrap a router Link. Takes an optional leading slot for an icon before the label.
BreadcrumbPage[data-slot="breadcrumb-page"]The current page — NOT a link (aria-current="page"). Also takes a leading slot.
BreadcrumbSeparator[data-slot="breadcrumb-separator"]Decorative divider between crumbs (defaults to a direction-aware chevron); aria-hidden / role=presentation. In a collapse mode the list inserts these itself — set the glyph with BreadcrumbList separator instead.
BreadcrumbEllipsis[data-slot="breadcrumb-ellipsis"]Collapsed-middle indicator for MANUAL trails; sr-only "More" label with an aria-hidden icon. (The maxItems and collapse modes render their own trigger instead.)
Props
The component's public props and their types.
The root has no props of its own — it's a thin wrapper over its native element. Its parts carry the API:
BreadcrumbList
collapse- Description
- Width-aware collapse: `"responsive"` menus hidden middle crumbs; `"back"` shows the immediate parent when the trail does not fit. Omit for manual / `maxItems` behaviour.
- Type
BreadcrumbCollapse
dir- Description
- Reading direction. Omit to inherit the ambient direction.
- Type
"ltr" | "rtl"
itemsAfterCollapse- Description
- Trailing items to keep visible when collapsed. Floored at 1.
- Type
number- Default
1
itemsBeforeCollapse- Description
- Leading items to keep visible when collapsed. Floored at 1.
- Type
number- Default
1
maxItems- Description
- Fold the middle when the item count exceeds this.
- Type
number
overflowLabel- Description
- Accessible name for the responsive "…" menu trigger.
- Type
((count: number) => string) | string- Default
(n) => `Show ${n} more`
separator- Description
- Divider auto-inserted in collapse modes. Manual mode ignores this prop.
- Type
React.ReactNode
Plus 282 inherited native HTML attributes.
BreadcrumbLink
asChild- Description
- Render the single child via Radix `Slot` instead of an `<a>`.
- Type
boolean- Default
false
leading- Description
- Content placed before the label. Icon-only crumbs need an accessible name.
- Type
React.ReactNode
Plus 288 inherited native HTML attributes.
BreadcrumbPage
leading- Description
- Content placed before the label. Icon-only pages need an accessible name.
- Type
React.ReactNode
Plus 280 inherited native HTML attributes.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="breadcrumb"][data-slot="breadcrumb-leading"][data-slot="breadcrumb-overflow-trigger"][data-slot="breadcrumb-expand-trigger"]States
| Selector | State | Description |
|---|---|---|
[data-collapse] | collapse | Reflects the active mode on BreadcrumbList: `manual` (consumer writes separators), `static` (maxItems, expand-in-place button), `responsive` (width-aware menu of hidden ancestors), or `back` (single parent link below the fit width). In a measured mode at an extreme-narrow width, the current-page label truncates (min-w-0 + ellipsis) rather than wrapping or dropping an anchor. |
| Property | Token | Tier |
|---|---|---|
itemGap | --garn-gap-inline | semantic · air |
triggerSize | --garn-control-h-sm | semantic · air |
clipMargin | --garn-space-6 | semantic · air |
When to use
Show where the current page sits in a hierarchy and let users jump to an ancestor — especially a deep trail in a header that must fold to fit as the space narrows.
Reach for something else when
- A flat site with no hierarchy
- the primary navigation (use a NavigationMenu or Tabs)
- steps in a linear flow (use a Stepper). For a global jump-to-anything surface, use Command.
Overview
Breadcrumb shows where the current page sits in a hierarchy and lets users jump back to an ancestor. Breadcrumb is the nav landmark, BreadcrumbList the ordered trail, BreadcrumbLink each ancestor link (asChild to wrap a router Link), and BreadcrumbPage the current page — rendered as text with aria-current, not a link. It's a secondary wayfinding aid for deep hierarchies; for primary navigation use NavigationMenu, and for a multi-step task use a Stepper.
Its one real failure mode is space, so BreadcrumbList folds a deep trail three ways. maxItems is the static count — over the limit it hides a fixed number of middle crumbs behind an expand-in-place "…" button. collapse="responsive" is width-aware — it measures the room the header actually gives it and folds only as many middle crumbs as needed so the trail never wraps, restoring them as the window grows; the collapsed "…" becomes a real dropdown menu of the hidden ancestors, each a working link. collapse="back" is the mobile move — below the fit width the whole trail becomes a single "‹ Parent" link to the immediate parent, the web equivalent of an app's Up button. Every collapse mode keeps the root and the current page and inserts its own separators; you pass items only.
Each crumb can carry a leading slot — a home glyph on the root, a file-type or entity icon on a resource path — before its label. A decorative icon is aria-hidden; a crumb that is only an icon must name itself (aria-label or sr-only text), the one gap most breadcrumb components leave open. And because a breadcrumb is a search rich-results surface, the à-la-carte breadcrumbListJsonLd helper turns the same array you map to crumbs into schema.org BreadcrumbList structured data for a <script type="application/ld+json">.
Guidelines
Use breadcrumbs only for genuine hierarchy. They earn their place when pages nest several levels deep; on a flat site they're noise. Mirror the real structure, root to current, so the trail matches how users got there.
The last crumb is the page, not a link. Render the current location as BreadcrumbPage (non-interactive, aria-current="page") and make every ancestor a working link. Keep labels short and consistent with the page titles.
Let a collapse mode do the work; don't wrap. Pass maxItems for a simple fixed fold, collapse="responsive" for a header breadcrumb that folds to whatever width it's given, or collapse="back" for a phone-width back-to-parent. Keep every crumb in the markup and let the mode decide what folds — the root and current page always stay. Width-driven modes measure their container, so they fold correctly inside a sidebar or split pane, not just the full viewport.
Name an icon-only crumb. A leading icon with no text (a bare home glyph) needs an aria-label or sr-only text — a decorative icon carries no accessible name on its own.
Best practices
- Mark the current page with BreadcrumbPage (it sets aria-current="page").
- Wrap your router's Link with BreadcrumbLink
asChildfor client navigation. - Fold long trails with a collapse mode instead of wrapping:
maxItems(static),collapse="responsive"(width-aware menu), orcollapse="back"(mobile parent link). - Give an icon-only crumb an aria-label (or sr-only text); pass a decorative leading icon as aria-hidden.
- Emit BreadcrumbList JSON-LD with breadcrumbListJsonLd for search rich results — from the same array you map to crumbs.
- Don't make the current page a link.
- Don't use breadcrumbs as the only navigation.
- Don't add a separator as a real list item — it's decorative, and a collapse mode inserts its own.
- Don't set itemsBeforeCollapse / itemsAfterCollapse to 0 — the root and current page must always stay (they're floored at 1).
Content guidelines
- Use each crumb's real page title, shortened consistently when it's long.
- Keep the current page as plain text, phrased the same way as the page's own title.
- Prefer a direction-neutral custom separator (a slash) — a custom chevron is yours to flip under RTL.
Troubleshooting
collapse="responsive" or "back" never folds in a preview / embedded frame, even when the container is clearly too narrow.
Cause. The measured collapse re-folds on a ResizeObserver signal, and some hidden/embedded documents never fire ResizeObserver (the first layout-effect measurement still runs, but later width changes don't).
Fix. This is environmental, not a bug — in a real browser it folds on resize. Verify width-driven behavior headlessly (a real layout), not in a hidden preview frame.
Passing both maxItems and collapse="responsive" — the maxItems cap seems ignored.
Cause. Width is the constraint in responsive mode, so it wins and maxItems is intentionally ignored (a fixed cap would re-introduce the wrong-at-most-widths problem).
Fix. Pick one mode. Use maxItems for a static fold, collapse="responsive" for width-aware — not both.
A crumb that's just a home/entity icon is flagged in dev as having no accessible name.
Cause. A leading icon is decorative (aria-hidden), so an icon-only crumb has no accessible name of its own.
Fix. Give the crumb an aria-label (or sr-only text). A crumb with a text label plus a decorative leading icon is fine as-is.
itemsBeforeCollapse={0} (or itemsAfterCollapse={0}) doesn't hide the root / current page as expected.
Cause. The root and current page are anchors the trail must never drop, so the keep-counts are floored at 1.
Fix. This is intended (and dev-warned). Keep both anchors; collapse only the middle.
A crumb's leading component (e.g. a router Link that prefetches, or an on-mount effect) runs twice under a measured collapse.
Cause. collapse="responsive" / "back" measure widths by rendering an off-screen mirror of the full trail, so every crumb's content mounts a second time. It's aria-hidden + inert (out of the a11y tree, unfocusable), but React still mounts it and runs its effects.
Fix. Keep crumb content side-effect-free — a plain label plus an optional decorative icon. If a crumb wraps a prefetching Link, expect one extra prefetch from the mirror, or drop to manual / maxItems collapse (no measurement mirror) where that matters.
breadcrumbListJsonLd output is missing item on the last crumb, or a URL is silently absent.
Cause. The last / current crumb omits item by design (search fills it from the page URL), and a non-absolute URL is dropped with a dev warning.
Fix. That's correct — give the current page no url, and pass absolute URLs (https://…) for the ancestors.
Accessibility
- Role
navigation- ARIA APG
- breadcrumb
- Focus
- Links and the "…" trigger are in the natural tab order (the trigger takes the shared focus ring). The responsive overflow menu is a dropdown-menu: opening moves focus into it, Escape closes it and returns focus to the trigger.
Accessibility requirements
The current page renders as BreadcrumbPage with aria-current="page" (never a link) through every collapse state.
when always
The root crumb keeps its link role through every collapse state — it is never folded into the menu or dropped.
when a collapse mode
The "…" trigger is a <button> whose accessible name reflects the hidden count (or the overflowLabel override) and whose glyph is aria-hidden.
when collapse="responsive" with hidden crumbs
The menu lists every hidden ancestor as a keyboard-reachable menu-item link with the correct href — none are dropped.
when the responsive overflow menu is open
Opening it from the keyboard moves focus into the menu; Escape closes it and returns focus to the "…" trigger.
when the responsive overflow menu
The full trail renders and stays navigable (it may wrap) — it never collapses to empty against a 0-width first layout.
when a measured collapse before first measurement (SSR / no-JS)
The off-screen measurement mirror is aria-hidden + inert: its duplicated crumbs are absent from the accessibility tree and unfocusable, and a consumer id / ref / aria-current is stripped from the clone.
when a measured collapse
No further crumb is collapsed; the current-page label truncates instead of wrapping or dropping an anchor.
when a measured collapse at a width where even root + "…" + current overflow
Separators (default or a custom `separator` node) stay decorative — aria-hidden / role=presentation — and are never real list items.
when always
The default separator points in the reading direction and the overflow menu opens on the mirrored side.
when dir="rtl"
Supplies a non-empty accessible name via aria-label / aria-labelledby / title or sr-only text (a decorative icon alone is not a name).
when a crumb whose only content is a leading icon (no text child)
Keep-counts are floored at 1 so the root and current page always stay visible; a 0 is coerced (and dev-warned).
when a collapse mode with itemsBeforeCollapse or itemsAfterCollapse below 1
The collapsed trail stays a nav landmark: <nav aria-label> + <ol> with the immediate parent as one <li> <a href> — never degraded to a bare anchor. At the root (no parent) it renders the current page only, no back link.
when collapse="back" below the fit width
Keyboard
| Tab | Move between the ancestor links (and onto the "…" trigger). |
| EnterSpaceArrowDown | On the responsive "…" trigger, open the menu of hidden ancestors. |
| ArrowUpArrowDown | Move between items inside the open overflow menu. |
| Enter | In the open menu, navigate to the focused ancestor link. |
| Escape | Close the open overflow menu and return focus to the "…" trigger. |
- The root <nav> is labeled "breadcrumb" so it's a distinct landmark — preserved in every collapse mode, including `back`.
- The current page is BreadcrumbPage (aria-current="page"), not a link, through every collapse state; the root keeps its link role.
- Separators are aria-hidden / role=presentation — decorative. The default chevron points in the reading direction and flips under RTL; a custom `separator` node is taken verbatim.
- The responsive "…" trigger is a <button> with a count-bearing accessible name (overflowLabel); its glyph is aria-hidden. Its menu items are the hidden ancestor links, keyboard-reachable, with real hrefs.
- The manual BreadcrumbEllipsis carries an sr-only "More" label (its icon is aria-hidden).
- A crumb that renders ONLY a leading icon (no text) must supply an accessible name via aria-label or sr-only text — a decorative icon has none.
- The off-screen measurement mirror is aria-hidden + inert, so its duplicated crumbs never reach the a11y tree or receive focus.
- Status
- Stable
- Version
v0.5.0- Introduced
v0.1.0
garn doesn't yet record a per-release version history for individual components. This reflects the component's current lifecycle state; a full per-release history is coming.