Skip to content
garn

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.

Stablev0.5.0@garn/ui/breadcrumb
On 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.

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.

terminal
npx garn-ui add breadcrumb
First time? Set up garn in your project
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

SelectorState
[data-collapse]collapse
PropertyTokenTier
itemGap--garn-gap-inlinesemantic · air
triggerSize--garn-control-h-smsemantic · air
clipMargin--garn-space-6semantic · 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

Do
  • Mark the current page with BreadcrumbPage (it sets aria-current="page").
  • Wrap your router's Link with BreadcrumbLink asChild for client navigation.
  • Fold long trails with a collapse mode instead of wrapping: maxItems (static), collapse="responsive" (width-aware menu), or collapse="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
  • 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

error

The current page renders as BreadcrumbPage with aria-current="page" (never a link) through every collapse state.

when always

error

The root crumb keeps its link role through every collapse state — it is never folded into the menu or dropped.

when a collapse mode

error

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

error

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

error

Opening it from the keyboard moves focus into the menu; Escape closes it and returns focus to the "…" trigger.

when the responsive overflow menu

error

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)

error

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

error

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

error

Separators (default or a custom `separator` node) stay decorative — aria-hidden / role=presentation — and are never real list items.

when always

error

The default separator points in the reading direction and the overflow menu opens on the mirrored side.

when dir="rtl"

error

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)

warn

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

error

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

TabMove between the ancestor links (and onto the "…" trigger).
EnterSpaceArrowDownOn the responsive "…" trigger, open the menu of hidden ancestors.
ArrowUpArrowDownMove between items inside the open overflow menu.
EnterIn the open menu, navigate to the focused ancestor link.
EscapeClose the open overflow menu and return focus to the "…" trigger.
A1.3.1Info and RelationshipsAAA2.4.8LocationA4.1.2Name, Role, ValueA2.1.1Keyboard
  • 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.