Bridge view

Breadcrumbs

A horizontal trail of links representing the user's location within a hierarchical site or app. Wrapped in a navigation landmark (`<nav aria-label="Breadcrumb">`), rendered as an ordered list with the last item marked `aria-current="page"` and rendered as static text (not a link). Separators are decorative and hidden from AT.

Also called Breadcrumb trail Page hierarchy Path navigation

When to use

Use

For multi-level hierarchical sites where users need orientation plus quick access to parent pages — documentation, e-commerce category trees, file managers, settings pages with nested sections. Place at the top of the page, before main content, so the "Skip to main content" link bypasses the trail. Trail variant when the hierarchy has three or more meaningful levels; back-link variant when only the parent matters.

Avoid

For flat-structure sites with only one or two levels — a single back-link or page title is sufficient. For linear journeys or transactions (multi-step forms, checkout flows) — that is `Stepper`, not breadcrumbs. For in-page section navigation — that is `Tabs`. For persistent global navigation — that is `SidebarNav`. Never use breadcrumbs to suggest a flow the user has not actually taken (breadcrumbs reflect site structure, not user history).

Versus related

  • sidebar-nav

    `SidebarNav` is a persistent vertical navigation rail listing the full top-level destination set; `Breadcrumbs` is a horizontal trail showing the current location within a hierarchy. SidebarNav answers "where can I go?"; Breadcrumbs answers "where am I?". Many sites pair both — the sidebar lists destinations, the breadcrumbs locate the current page within the chosen destination's hierarchy.

  • tabs

    `Tabs` switches between sibling sections of the same page (overview / activity / files for a single project); `Breadcrumbs` traverses parent pages of a hierarchy. Tabs operate within a single context; breadcrumbs cross contexts. The decision test: does activating the affordance change the page (breadcrumbs) or change a section of the current page (tabs)?

  • stepper

    `Stepper` represents a linear sequence the user moves through (checkout: cart → address → payment → review); `Breadcrumbs` represents site-structure parents (Home → Settings → Account). Stepper has progress semantics (steps before are completed, steps after are pending); breadcrumbs has only location semantics. Never use breadcrumbs for a flow.

  • pagination

    `Pagination` orients users within one list's page-window (results 1-20 / 21-40); `Breadcrumbs` orients users within site hierarchy (Home → Settings → Account). Pagination stays in one site context (URL query changes, not the path); breadcrumbs cross site contexts (URL path changes). They commonly co-occur — breadcrumbs at the top of the page locate the current section, pagination below the data navigates pages within that section.

Breadcrumbs communicate "you are here" plus "how you got here" in one row. Two variants codify the canonical decision: a full trail (`Home › Settings › Account › Email`) for multi-level hierarchies, or a single back-link (`← Settings`) for app pages where only the parent is meaningful. The reference documents the navigation- landmark contract, the `aria-current="page"` rule on the last item (which is canonical static text, not a link), the decorative-separator pattern, and the truncation contract for long trails — first item plus collapse menu plus current item.

Highlight
Fig 1.1 · Breadcrumbs · Bridge view

Implementations

How specific libraries realise the canonical anatomy. Each entry records the deltas between the canon and the library's surface.

react-aria Breadcrumbs
import {
Breadcrumbs,
Breadcrumb,
Link,
} from 'react-aria-components';
import { ChevronRight } from 'lucide-react';
// Static JSX — full trail
<nav aria-label="Breadcrumb">
<Breadcrumbs onAction={(key) => navigate(key)}>
<Breadcrumb id="home">
{({ isCurrent }) => (
<>
<Link href="/">Home</Link>
{!isCurrent && <ChevronRight size={14} aria-hidden="true" />}
</>
)}
</Breadcrumb>
<Breadcrumb id="settings">
{({ isCurrent }) => (
<>
<Link href="/settings">Settings</Link>
{!isCurrent && <ChevronRight size={14} aria-hidden="true" />}
</>
)}
</Breadcrumb>
<Breadcrumb id="account">
{({ isCurrent }) => (
<>
<Link>Account</Link>
</>
)}
</Breadcrumb>
</Breadcrumbs>
</nav>
// Dynamic collection
<nav aria-label="Breadcrumb">
<Breadcrumbs items={crumbs} onAction={(key) => navigate(key)}>
{(item) => (
<Breadcrumb id={item.id}>
{({ isCurrent }) => (
<>
<Link href={isCurrent ? undefined : item.href}>{item.label}</Link>
{!isCurrent && <ChevronRight size={14} aria-hidden="true" />}
</>
)}
</Breadcrumb>
)}
</Breadcrumbs>
</nav>

Divergence

From Type → To Rationale
anatomy[root] reshaped Consumer-supplied <nav aria-label="..."> wrapping Breadcrumbs (renders <ol>) The canonical root is a `<nav aria-label="Breadcrumb">` landmark that wraps the ordered list. React Aria splits this across two layers: `Breadcrumbs` renders only the `<ol>` element; the `<nav>` landmark is not generated automatically by the component. The docs state "When breadcrumbs are used as a main navigation element for a page, they can be placed in a navigation landmark" — the consumer must supply the `<nav aria-label="...">` wrapper themselves. This shifts the landmark contract from the library to the consumer, creating a risk that implementations ship a bare `<ol>` without the required landmark. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
anatomy[list] reshaped Breadcrumbs component renders <ol class="react-aria-Breadcrumbs"> The `list` slot is the `<ol>` rendered by the `Breadcrumbs` component itself — there is no separate named List child component. The class `react-aria-Breadcrumbs` is the default CSS hook. This collapses the canonical two-level anatomy (root wraps list) into a single component that IS the list, with the root landmark delegated to the consumer. Source: https://github.com/adobe/react-spectrum/blob/main/packages/react-aria-components/src/Breadcrumbs.tsx (fetched 2026-05-05)
anatomy[item] renamed Breadcrumb (renders <li class="react-aria-Breadcrumb">) The canonical `item` slot maps directly to the `Breadcrumb` component, which renders an `<li>`. The rename from generic `item` to `Breadcrumb` is idiomatic in React Aria — named per-item components (matching the pattern of `Disclosure`, `Tab`, `ListBoxItem`) rather than a generic `Item`. The `id` prop on `Breadcrumb` serves as the key passed to `onAction`, enabling SPA router navigation without href interception. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
anatomy[link] reshaped React Aria Link component inside Breadcrumb children; last item's Link receives aria-current="page" via LinkContext when isCurrent is true The canonical `link` slot is an `<a>` for intermediate items. React Aria uses its own `Link` component (from react-aria-components) inside the `Breadcrumb` children. The `Link` receives `aria-current="page"` automatically via `LinkContext` when the breadcrumb is the current item (`node.nextKey == null`). Consumers use the `isCurrent` render prop to conditionally omit the `href` from the current item's Link, converting it from an active anchor to a non-navigating element. If the consumer passes `href` on the current item's Link, React Aria still applies `aria-current="page"` but the link remains interactive — the canonical rule (no href on current item) is a consumer responsibility, not enforced by the library. Source: https://react-aria.adobe.com/Breadcrumbs#anatomy (fetched 2026-05-05)
anatomy[current-marker] reshaped Link with isCurrent=true (data-current attribute) receiving aria-current="page" via LinkContext; no separate <span> element The canonical `current-marker` is a static `<span aria-current="page">` (not a link). React Aria has no distinct current-marker component — the final `Breadcrumb` item's `Link` child receives `data-current` and `aria-current="page"` via the context, making it a styled link rather than a non-interactive span. The consumer is responsible for removing the `href` on the current item's Link to prevent a self-link; the library does not enforce this. This is a significant deviation from the canonical non-link pattern. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
anatomy[separator] reshaped Consumer-composed element (e.g. ChevronRight icon with aria-hidden) inside Breadcrumb children render function, conditionally rendered via {!isCurrent} The canonical separator is a decorative slot managed by the component (rendered as CSS pseudo-element or `aria-hidden` element). React Aria ships no built-in separator. The documented pattern places a chevron icon (e.g. from lucide-react) inside each `Breadcrumb`'s children render function, conditionally shown only when `!isCurrent`. The consumer must apply `aria-hidden="true"` to the icon manually; there is no library-enforced accessibility contract on the separator. This moves the separator from a component slot to a consumer convention. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
anatomy[overflow-collapse] omitted React Aria Breadcrumbs has no built-in overflow-collapse mechanism. There is no `maxItems`, `itemsBeforeCollapse`, or `itemsAfterCollapse` prop, and no `…` button that opens a menu of collapsed items. The canonical overflow-collapse slot and its `button-with-aria-expanded` semantic are absent entirely. Consumers needing trail truncation must compose their own overflow button and collapsed-items menu on top of the primitive. This is a significant omission for deep hierarchies. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
axes.variants[back-link] omitted React Aria does not ship a back-link variant. The library models breadcrumbs as a collection-based trail only; a single-item "← Parent" back-link pattern is not a first-class variant. Consumers can construct it by rendering a single `Breadcrumb` with a Link containing a back arrow label, but the library provides no dedicated API or variant prop for this pattern. The canonical `back-link` variant is undocumented in React Aria. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
axes.properties[size] omitted React Aria is an unstyled primitive library. No `size` prop (sm / md / lg) is shipped on `Breadcrumbs` or `Breadcrumb`. Text size is the consumer's CSS responsibility, applied via the default class selectors (`.react-aria-Breadcrumbs`, `.react-aria-Breadcrumb`) or custom `className`. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
axes.properties[expandable] omitted No `expandable` prop and no overflow-collapse mechanism. Since there is no built-in truncation (see `anatomy[overflow-collapse]`), the expand/ collapse axis does not exist in this library. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
axes.properties[showRoot] omitted No `showRoot` prop. The concept of pinning the root item during overflow-collapse does not exist because overflow-collapse itself is absent. All rendered `Breadcrumb` items are always visible. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
axes.states.data[collapsed] omitted No collapsed state — there is no overflow-collapse mechanism in React Aria Breadcrumbs, so the collapsed/expanded axis does not apply. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
axes.states.data[expanded] omitted Same as collapsed — the expanded state only exists relative to the overflow-collapse button, which React Aria does not ship. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
axes.states.interactive[hover] extended + React Aria exposes `data-hovered` on the `Link` component (via `useHover`), enabling pointer-device hover styling without CSS `:hover`. This avoids `:hover` stickiness on touch devices. The canonical documents hover as a CSS state; React Aria surfaces it as a data attribute. React Aria normalises hover across pointer types using its internal `useHover` hook. `[data-hovered]` is set on the Link; consumers style from `[data-hovered]` rather than `:hover`. This is an additive extension — the canonical hover state is fully covered and the data attribute is an additional hook. Source: https://react-aria.adobe.com/Breadcrumbs#styling (fetched 2026-05-05)
axes.states.interactive[focus-visible] extended + React Aria exposes `data-focus-visible` on the `Link` component (via `useFocusVisible`). The canonical documents `focus-visible` as a CSS pseudo-class; React Aria additionally surfaces it as a data attribute for consumers who need JS-driven focus ring logic or cannot rely on the `:focus-visible` CSS pseudo-class. React Aria's `useFocusVisible` tracks keyboard-vs-pointer focus intent and sets `data-focus-visible` on the link element. The CSS hook is more reliable than `:focus-visible` polyfills and is consistent with how React Aria exposes focus state across all interactive primitives. Source: https://react-aria.adobe.com/Breadcrumbs#styling (fetched 2026-05-05)
axes.states.transitions[collapsed-to-expanded] omitted The collapsed→expanded transition (overflow-collapse menu opens) does not exist because React Aria ships no overflow-collapse mechanism. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
axes.states.transitions[expanded-to-collapsed] omitted Same as collapsed→expanded — absent because overflow-collapse is absent. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
events[itemActivate] renamed onAction(key: Key) on Breadcrumbs The canonical `itemActivate` event (fired with `{ item, index }`) maps to `onAction` on the `Breadcrumbs` container. `onAction` receives only the `Key` of the activated breadcrumb (the `id` prop value), not the full item object or index. This follows the standard React Aria Collection pattern (same shape as `ListBox.onAction`, `Menu.onAction`). Consumers derive the full item from the key by looking up their items array. The `index` and full item payload in the canonical event are not present — key-only. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
events[expandedChange] omitted No `expandedChange` / `onExpandedChange` event — the overflow-collapse mechanism that fires this event is absent from React Aria Breadcrumbs. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
anatomy[root] extended + React Aria adds an `isDisabled` prop on `Breadcrumbs` that disables all breadcrumb links simultaneously (propagated to children via `LinkContext`). The canonical anatomy defines no component-level disable axis for breadcrumbs — individual intermediate items are simply omitted from the trail, not disabled. This is an additive capability with no canonical equivalent. React Aria's collection-based model allows a single `isDisabled` toggle on the container to propagate disabled state to all child `Link` components via context, matching the library-wide pattern of `ListBox.isDisabled`, `Menu.isDisabled`, etc. It is a convenience affordance for forms or conditional states where the entire trail becomes non-interactive; the canonical does not model this because breadcrumbs in non-interactive state are typically hidden rather than disabled. Source: https://react-aria.adobe.com/Breadcrumbs (fetched 2026-05-05)
Why this audit reads the way it does

React Aria Breadcrumbs (`Breadcrumbs` + `Breadcrumb` + `Link` from react-aria-components) covers the core interactive contract — ordered list semantics, aria-current on the last item, keyboard-navigable links, and onAction for SPA routing — but diverges from the canonical anatomy in three structural ways: 1. No auto-generated nav landmark. The library renders `<ol>` only; the `<nav aria-label="...">` wrapper must be supplied by the consumer. This shifts the landmark accessibility contract onto every consumer integration. 2. No distinct current-marker element. The last Breadcrumb's Link receives aria-current="page" via context, but the consumer must also remove the href to satisfy the canonical non-link rule. The library does not enforce the href-omission — a consumer passing href on the current item creates a self-link, which is the canonical mistake breadcrumbs-current-is-link. 3. No overflow-collapse mechanism. The canonical overflow-collapse slot (a button that expands collapsed middle items) is entirely absent. Deep trails render all items with no truncation. Consumers must compose truncation themselves. Beyond these structural gaps, React Aria follows its library-wide philosophy: no visual variant props (size, back-link variant), no motion tokens, and separator rendering delegated to consumer children. The data-attribute state hooks (data-current, data-hovered, data-focus-visible, data-disabled) are additive extensions that give consumers fine-grained CSS targeting.

Both

Figma↔Code mismatches

Where designer and developer worlds typically misalign on this component.

  1. 01
    Figma

    Breadcrumb separators drawn as inline text characters

    Code

    Separators rendered via CSS `::after` pseudo-element with `aria-hidden`

    Consequence

    Designers ship literal "/" or "›" characters as inline text between items; developers extract them to CSS pseudo-elements so they never appear in the accessibility tree. The two surfaces look the same but the design file does not communicate the AT-exclusion contract — developers may copy the inline text and accidentally expose the separator to SR.

    Correct

    Document the separator as a decorative pseudo-element in the anatomy. Figma uses a non-text decorative shape (chevron icon, vector slash) so the design surface signals "this is decorative". Code uses `::after { content: '\\203A' }` plus a fallback `aria-hidden="true"` if rendered as a real element.

  2. 02
    Figma

    Last breadcrumb item drawn as a styled link

    Code

    Last item rendered as `<span>` with `aria-current="page"` (no `<a>`)

    Consequence

    Designers draw the current page with link styling (underline, accent color); developers strip the anchor and ship a span. The two surfaces visually disagree (designer sees "underlined link to current page", code ships "muted static text") and the rationale is invisible in the Figma file — the next designer reverts the styling.

    Correct

    Document the current-marker as a non-link slot in the anatomy. Figma carries a "current" variant of the item component with non-link styling (muted color, no underline, optional weight bump). Code renders `<span aria-current="page">`.

  3. 03
    Figma

    Trail truncation drawn as static "..." text

    Code

    Truncation rendered as `<button>` opening a menu of collapsed items

    Consequence

    Designers draw "…" as a label between items; developers ship a button that opens a menu. The visual element looks static but is interactive — designers do not draw the hover, focus, or expanded states; developers ship them.

    Correct

    Document the overflow-collapse slot as `<button>` with menu-button states (default / hover / focused / expanded). Figma carries the button-state variants. Static truncation (no expansion) is the rare case — for short trails the canonical pattern is to show all items.

  4. 04
    Figma

    Separator character hard-coded as a chevron in LTR design files

    Code

    Separator follows `direction: rtl` mirroring or distinct character per direction

    Consequence

    Designers ship `>` separators in LTR files; the same file under RTL shows `>` pointing the wrong way (against reading direction). Sighted RTL users see "Home > Settings" but read right-to-left, so the chevron points away from the parent.

    Correct

    Document the separator as direction-aware in the anatomy. Figma carries both LTR (`›`) and RTL (`‹`) frames, or uses a chevron icon component that is mirrored via direction. Code uses CSS that swaps content per direction or relies on the bidi mirroring of the character.

Both

Variants, properties, states

Variants

Structurally different versions of the component.

trail back-link

Properties

The same component, parameterised.

PropertyType
size sm | md | lg
expandable boolean
showRoot boolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visible
data
defaultcurrentcollapsedexpanded
Both

Figma ↔ Code property map

FigmaKindCodeNotes
VariantEnumvariantMaps trail / back-link variant.
SizeEnumsizeMaps sm / md / lg sizes.
ExpandableBooleanexpandableWhen true, overflow-collapse menu allows revealing collapsed middle items.
Show RootBooleanshowRootWhen true, the root (Home) item is always visible even under collapse.
Max ItemsNumbermaxItemsBound on visible item count before overflow-collapse activates.
Items Before CollapseNumberitemsBeforeCollapseNumber of items shown at the start of the trail when collapsed.
Items After CollapseNumberitemsAfterCollapseNumber of items shown at the end of the trail when collapsed.
SeparatorSlotseparatorOptional override slot for the separator character or icon.
ItemsSlotitemRepeatable slot for child breadcrumb items.
Both

State transitions

FromToTrigger
collapsedexpandedUser activates overflow-collapse menu-button (click, Enter, Space)
expandedcollapsedUser dismisses menu (Escape, outside click, item selection)
Designer

Figma anatomy

Slot Figma type Hint
root frame Auto-layout horizontal frame; navigation landmark variant property
list frame Horizontal auto-layout; gap small; reset list-marker
item frame Inline frame holding link or current-marker plus separator
link text Link variant of breadcrumb item; underline on hover
icon-leading from icon-leading-text instance Icon component instance; size bound to host's size token
label from icon-leading-text text Text style bound to a "label" component property; truncates with ellipsis when single-line variant overflows
current-marker text Current variant of breadcrumb item; non-link styling
separator text Decorative separator; CSS pseudo-element preferred over inline character
overflow-collapse instance Menu-button instance with ellipsis label; visibility per "is collapsed" property
Dev

Code anatomy

Slot Code slot Semantic
root root nav-with-aria-label
list list ol
item item li
link link a
icon-leading from icon-leading-text icon-leading presentational-or-img
label from icon-leading-text label text
current-marker current-marker span-with-aria-current
separator separator presentational
overflow-collapse overflow-collapse button-with-aria-expanded
Dev

Cross-framework expression

FrameworkStructure mechanismVariant mechanism
Web Components A `<ui-breadcrumbs>` host with `variant`, `size`, `max-items`, `expandable` attributes; renders slotted `<ui-breadcrumb-item>` children with the last item auto-marked `aria-current="page"` attributes (`variant="trail"`, `variant="back-link"`, `size="md"`); CSS uses logical properties for separator direction and `::after` pseudo-elements for separator content
React Compound component (React Aria Breadcrumbs + Item, Material UI Breadcrumbs, HeroUI Breadcrumbs.Item, Atlassian Breadcrumbs); items declared as children, last child auto-receives `aria-current="page"` props with class-variance-authority; `maxItems` + `itemsBeforeCollapse` + `itemsAfterCollapse` (Material UI + Atlassian convention) drive truncation; `separator` prop overrides the default character
Angular (signals) Angular component with input<'trail' | 'back-link'>('variant'), content-projects child item components via ng-content with selector; signal-derived current item from index input<'sm' | 'md' | 'lg'>(); host-binding [attr.role]="'navigation'" plus [attr.aria-label]; signal-derived collapsed state from contentChildren count and maxItems
Vue Single-file component with default slot for breadcrumb items; PrimeVue Breadcrumb, Naive UI Breadcrumb as third-party precedents; computed property derives current-marker from last item defineProps with literal-union types; v-if for overflow-collapse rendering when items.length > maxItems
Both

Events

  1. itemActivateoptional
    Payload
    `{ item: { id: string, href: string }, index: number }`. Fires when a user activates a breadcrumb link via click, Enter, or Space. Only canonical when consumers manage navigation programmatically (SPAs that intercept link clicks for client-side routing); standard server-rendered breadcrumbs let the browser handle navigation natively without observing the event.
    Web Components
    `itemActivate` CustomEvent on the host with `event.detail = { item, index }`.
    React
    `onAction(key)` callback (React Aria idiom) — receives the activated item key.
    Angular Signals
    `output<{ item: BreadcrumbItem; index: number }>('itemActivate')`.
    Vue
    `@item-activate` event with payload `{ item, index }`.
  2. expandedChangeoptional
    Payload
    `{ expanded: boolean }`. Fires when the overflow-collapse menu opens or closes. Only canonical when the consumer controls the menu state externally; uncontrolled breadcrumbs manage state internally. Static (non- expandable) breadcrumbs never observe this event.
    Web Components
    `expandedChange` CustomEvent with `event.detail = { expanded }`.
    React
    `onExpandedChange(expanded)` callback.
    Angular Signals
    `output<boolean>('expandedChange')`.
    Vue
    `@expanded-change` event or `v-model:expanded` two-way binding.
Both

Internationalisation

RTL · mirroring

Trail order respects DOM order — first DOM item is the root regardless of language, but visual order flips under `direction: rtl` (root sits at the inline-start, which is right-side under RTL). Separator character mirrors via bidi (`›` / U+203A flips visually under `direction: rtl`) or via writing-direction-aware CSS that swaps to `‹` / U+2039. Document the chosen mechanism — either bidi mirror or distinct character per direction. Nav-landmark `aria-label` translates per locale ("Brotkrumen" for German, "Fil d'Ariane" for French) so SR announces it naturally.

Text expansion

Trail labels expand 30-50% under translation (English "Settings" → German "Einstellungen" 60% longer; French "Paramètres" 30% longer). Long trails plus expanded labels overflow the inline-size faster — the responsive breakpoint rules collapse earlier. Reserve no fixed width on items; let them flow and collapse naturally. Separator characters are stable across languages.

Both

Accessibility

Slot Accessibility hint
root `<nav aria-label="Breadcrumb">`. The label must be specific — "Breadcrumb" not "Navigation" — because a page may have multiple `<nav>` landmarks (primary nav, breadcrumbs, in- page TOC). AT users rely on the label to disambiguate. Translate the label per locale ("Brotkrumen" for German, "Fil d'Ariane" for French) so SR announces it in the user's language.
list `<ol>`. Some implementations omit the list and render items as inline anchors — this works for sighted users but loses the "list of N items" announcement that makes the trail navigable by SR. Keep the list element.
item `<li>` carries the position in the list. SR announces "1 of 4, link, Home; 2 of 4, link, Settings; 3 of 4, link, Account; 4 of 4, Email" — the position counter is canonical and helps users orient.
link `<a href="...">`. Standard link semantics — Tab focusable, Enter activates. Visible focus ring per WCAG 2.4.7. Hover and focus-visible underline (Polaris convention) so the link affordance is clear; default state may be underline-on-hover for visual cleanliness.
icon-leading `aria-hidden="true"` on the SVG when the icon is purely decorative (paired with a visible label). If the icon is the sole signal of the host's intent (icon-only host variant), the host carries an `aria-label` describing the action — the icon never announces itself.
label Plain text node; no special role. The label is the host's accessible name unless overridden by `aria-label` / `aria-labelledby`. Avoid visually-hidden modifiers — the visible text and the announced name should match.
current-marker `aria-current="page"` is canonical. The element MUST NOT be `<a href="...">` — `aria-current` plus a self-link creates a useless tab stop and the page-reload-on-click is hostile. Render as `<span aria-current="page">` or as `<a aria-current="page">` without `href`. SR announces "current page, Email".
separator `aria-hidden="true"` is canonical, or render via CSS pseudo-element (`::after`) which is automatically excluded from the accessibility tree. Never expose the separator character to AT — SR users hear "Home, Settings, Account, Email" not "Home slash Settings slash Account slash Email". RTL: use `content: '\\203A'` for `›` and let `direction: rtl` flip via mirroring, or write a logical- property rule that swaps to `‹` under RTL.
overflow-collapse `<button aria-label="Show collapsed breadcrumbs" aria-expanded="false" aria-controls="...">…</button>`. On activation, the menu opens and `aria-expanded` flips to true; focus moves into the menu. Escape closes and returns focus to the button. Never use a `<span>` with onClick — keyboard users cannot activate it.
Both

Accessibility acceptance

Keyboard walk

KeysExpected
TabFocus moves through each link slot in DOM order. The current-marker (last item) is NOT in the tab order because it is not a link. The overflow-collapse button is in the tab order between the first item and the post-collapse items.
Shift+TabReverse focus through the same elements. Standard link navigation — no breadcrumbs-specific behavior.
Enter, Space (overflow-collapse focused)Activates the menu-button. Menu opens; focus moves to the first item in the menu (or the menu container with `tabindex="-1"`). `aria-expanded` flips to true.
Escape (menu open)Closes the menu. Focus returns to the overflow-collapse button. `aria-expanded` flips back to false.
Arrow keys (menu open)Navigate between menu items (vertical: ArrowUp / ArrowDown for a `role="menu"` listing of collapsed breadcrumb items). Enter activates the focused item.

Screen-reader announcements

TriggerExpected
SR enters the breadcrumb landmarkSR announces "navigation, Breadcrumb" (the landmark plus its label). For deep trails, the announcement continues with "list, 4 items".
SR encounters each itemIntermediate items announce as "link, Home" / "link, Settings" / "link, Account". The current item announces as "current page, Email" — the link role is absent because the element is a span (or anchor without href).
User activates overflow-collapseSR announces the menu open ("menu, 3 items") and reads the first menu item. The button's `aria-expanded` flip is announced.

axe-core rules to assert

  • aria-allowed-attr
  • aria-valid-attr-value
  • aria-required-attr
  • landmark-unique
  • link-name

Same data as JSON for direct ingestion into Playwright + @axe-core/playwright or Jest + jest-axe: /api/components/breadcrumbs/a11y-fixture.json

Both

Contracts

Non-negotiable contracts

  1. APGAPG: Breadcrumb pattern

    The trail is wrapped in `<nav>` with a specific `aria-label` ("Breadcrumb" or the locale-translated equivalent). The label must distinguish the breadcrumb from other navigation landmarks on the page. Implementations that ship a bare `<ol>` lose landmark navigation entirely.

    Without the landmark, SR users cannot jump to the trail via landmark navigation — the "you are here" signal becomes invisible to AT. Sighted-only orientation breaks the canonical accessibility contract.

  2. APGAPG: Breadcrumb pattern

    The last item carries `aria-current="page"` and is NOT rendered as `<a href>`. Static text via `<span>` or `<a>` without `href` is canonical. Implementations that ship a self-link to the current page waste user effort and break the "current location" semantic.

    Without the rule, activating the current item reloads the page (lost scroll, lost form state, no progress). AT users hear "link, current page" — a contradiction that confuses the navigation model. The current page is not a navigation target.

  3. APGAPG: Breadcrumb pattern

    Separators are decorative — `aria-hidden="true"` or rendered via CSS pseudo-elements. Implementations that expose separator characters to AT pollute the trail announcement with non-semantic noise.

    Without the rule, SR reads "Home slash Settings slash Account slash Email" — the slash adds no value but lengthens every announcement. AT users learn to tune out the noise, eroding trust in the trail's semantic clarity.

Vocabulary drift

Material 3
Material 3 spec does not include Breadcrumbs as a first-class component (web canon ships Material UI Breadcrumbs as a library extension). The mobile-first Material guidance favours top-app-bar back-buttons over breadcrumbs for navigation orientation.
Atlassian
Breadcrumbs
Atlassian ships maxItems + itemsBeforeCollapse + itemsAfterCollapse as canonical truncation API; aligns with Material UI naming.
Carbon
Breadcrumb
Carbon uses singular "Breadcrumb" for both single item and trail composition; minor naming-only divergence from the canonical plural.
Polaris
Breadcrumbs
Polaris uses Breadcrumbs for a single back-link convention (single parent) — the canonical back-link variant captures this divergence as a first-class axis rather than treating it as a different component.
GOV.UK
Breadcrumbs
GOV.UK collapses to first-and-last on mobile (skip the middle entirely, no overflow-collapse menu) — the responsive breakpoint rule documents this as a canonical mobile pattern.
GitHub Primer
Breadcrumbs
Primer ships an `overflow` prop with `wrap` / `menu` / `menu-with-root` modes — same canonical truncation contract under different API names. `menu-with-root` keeps the root visible when other items collapse (matches the canonical `showRoot` property).
Both

Common mistakes

Blocker

#breadcrumbs-no-nav-landmark

Trail is not wrapped in `<nav>` with `aria-label`

Problem

The breadcrumb is rendered as a bare `<ol>` or a sequence of inline links with no enclosing landmark. SR users navigating by landmark cannot locate the trail; the "you are here" signal is invisible to AT.

Fix

Wrap the trail in `<nav aria-label="Breadcrumb">` (or the translated equivalent for non-English locales). The label must be specific — "Navigation" is too generic when the page has a primary nav landmark too. AT users rely on the label to disambiguate.

Major

#breadcrumbs-separator-readable

Separator characters are exposed to assistive technology

Problem

Inline separators (`/`, `>`, `›`) are rendered as real text nodes without `aria-hidden`. SR reads "Home slash Settings slash Account slash Email" — the slash is noise that lengthens the trail announcement and adds no semantic value.

Fix

Render separators via CSS `::after { content: '...' }` (the pseudo-element content is excluded from the accessibility tree by canonical browser behavior) or wrap inline separator characters in a `<span aria-hidden="true">`. SR announces the trail as a clean list of items.

Major

#breadcrumbs-rtl-separator-not-mirrored

Separator character does not flip under RTL

Problem

The trail uses `>` or `›` separators that do not visually mirror under `direction: rtl`. Sighted RTL users see the chevron pointing left-to-right, against the reading direction — the visual hierarchy reads backward.

Fix

Use a CSS pseudo-element with logical content (`::after { content: '\\203A' }` plus `direction: ltr` content override under RTL), or use a chevron icon component that mirrors via the `direction` cascade, or swap characters via writing-direction-aware CSS rules. Document the chosen mechanism in the canonical anatomy.

Major

#breadcrumbs-collapse-no-disclosure

Truncated middle items are not reachable

Problem

The trail truncates middle items to fit the viewport (`Home … Email`) but the ellipsis is static text — there is no way to reveal the collapsed items. Users with deep hierarchies cannot navigate to mid-level parents without visiting them via another path.

Fix

Render the ellipsis as `<button aria-expanded="false">` that opens a menu listing the collapsed items. Activating the button moves focus into the menu; selecting an item navigates. Escape closes and returns focus to the button. Static truncation is acceptable only when the collapsed items are reachable via another path; otherwise the disclosure is mandatory.