Skip to content
Navigation

Breadcrumb

The trail to the current page. Routing is yours — the link part is a styled anchor, not a router link.

Added 14 October 2026

Routing is the consumer’s

BreadcrumbLink / hlmBreadcrumbLink is a styled <a> and nothing more. Put your own routing on it:

<BreadcrumbLink href="/campaigns">Campaigns</BreadcrumbLink>
<a hlmBreadcrumbLink routerLink="/campaigns">Campaigns</a>

Upstream spartan helm composes Angular’s RouterLink into the link, which would make @angular/router a dependency of this package for every Angular consumer — the ./angular bundle imports its peers at top level, so even an app that never renders a breadcrumb would need it. React has no such coupling, so both sides stay router-agnostic and the two APIs keep the same shape.

Structure

<nav hlmBreadcrumb aria-label="Campaign breadcrumb">
  <ol hlmBreadcrumbList>
    <li hlmBreadcrumbItem><a hlmBreadcrumbLink href="/campaigns">Campaigns</a></li>
    <li hlmBreadcrumbSeparator></li>
    <li hlmBreadcrumbItem aria-hidden="true"><hlm-breadcrumb-ellipsis /></li>
    <li hlmBreadcrumbSeparator></li>
    <li hlmBreadcrumbItem><span hlmBreadcrumbPage>Line items</span></li>
  </ol>
</nav>
  • The last crumb is BreadcrumbPage — a plain <span aria-current="page">. Not a link, because it goes nowhere.
  • BreadcrumbSeparator renders a chevron by default; pass children to override (/, ›). It is aria-hidden, which also keeps the <ol>’s item count honest.
  • BreadcrumbEllipsis collapses a middle section. Put aria-hidden on its wrapping item too — otherwise the list reports a counted, empty entry. [a11y]
  • Override aria-label when a page has more than one breadcrumb. Identical landmark names are indistinguishable in a landmark list.

Deviations from upstream, both accessibility fixes

  • The current crumb is not role="link". Upstream adds role="link" aria-disabled="true"; we keep only aria-current="page". aria-disabled is inert on an element nothing can focus, and the role puts an unusable entry in a screen reader’s links list. The crumb is in the trail because of the nav → ol → li structure — the role never contributed to that.
  • Links are underlined and the current crumb is font-medium. Upstream distinguishes them by colour alone (text-muted-foreground vs text-foreground), which in the dark theme are 1.15:1 apart — effectively identical. Colour must not be the only carrier (WCAG 1.4.1), so there is a weight difference and an underline; the underline doubles as the link affordance.

Known token debt: the links sit on --muted-foreground, which is 4.26:1 on the page background — just under the 4.5:1 that 14px text needs. Fixing it means darkening the token, which affects every muted text in the library, so it is tracked in scripts/check-contrast.mjs rather than patched here.

Rules

  • Breadcrumbs reflect hierarchy, not history. If it changes depending on where the user came from, it is not a breadcrumb.
  • Three to five crumbs. Collapse the middle with the ellipsis rather than wrapping to two lines.
  • Don’t repeat the page’s h1 as the last crumb and the heading — one of them is enough; keep the heading.

Never

  • A clickable current page.
  • A breadcrumb as the only navigation — it is orientation, not a menu.
  • A separator carrying meaning (> vs / implying something). It is aria-hidden; nobody hears it.