ComponentsActions & navigationBreadcrumb
On this page
Actions & navigation

Breadcrumb released

The trail to the current page, told in links, with the page itself at the end of it.

  • Import@pacit/components/breadcrumb
  • Selectorspct-breadcrumb, pct-crumb, a[pctCrumbLink]

Implements the W3C ARIA APG Breadcrumb pattern ↗

breadcrumb.demo.ts — the same file runs here and is highlighted under Code

Start here

Usage

One import per component, and the shortest possible use — the package itself is installed once, on the start page.

Import this component
import { PctBreadcrumb, PctCrumb, PctCrumbLink } from '@pacit/components/breadcrumb';
ExportedWhat it is
PctBreadcrumbcomponentpct-breadcrumbA breadcrumb: the way here, told in the platform's own links.
PctCrumbcomponentpct-crumbOne step of the trail: role="listitem", and the separator drawn before its content.
PctCrumbLinkcomponenta[pctCrumbLink]The trail's link, standing on the consumer's own anchor.
<pct-breadcrumb ariaLabel="You are here">
  <pct-crumb><a pctCrumbLink href="/">Home</a></pct-crumb>
  <pct-crumb><a pctCrumbLink href="/data" aria-current="page">Data</a></pct-crumb>
</pct-breadcrumb>
Examples

Every state, running

Each example is the component itself, not a screenshot. Open the code under any of them.

A long way here

The trail is an ordered list of links; only the last crumb carries aria-current="page", and it is a link like the others so the current page is reachable by keyboard too.

API

What it reads, and what it writes back

Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.

A breadcrumb: the way here, told in the platform's own links.

Inputs

NameTypeWhat it does
ariaLabeldefault ''stringThe accessible name of the landmark. Overrides texts().breadcrumbLabel, whose job is only to carry a sensible default in the application's language.

Outputs

None of its own. The native events — (click), (focus), the form's submit — are the platform's, and two-way models above carry the rest.

What it writes on the element

The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.

AttributeBound toNote
classpct-breadcrumb
rolenavigationA named navigation landmark — the pagination's reasoning one component over: the landmark is how a screen-reader user reaches the trail and steps over it, and it has to be named because two trails on one page are two landmarks.
aria-labelariaLabel() || texts().breadcrumbLabel
Styling

Parts to select, tokens to override

Two ways in, both stable. No class names to depend on.

Parts

In a test, PctBreadcrumbHarness, PctCrumbHarness, PctCrumbLinkHarness from @pacit/components/testing read a part by the name below and a state by its attribute — the harness gate holds the names to the package.

PartSelectorWhat it is
list[data-pct-part="list"]the ordered list of crumbs
separator[data-pct-part="separator"]the mark between two crumbs

Tokens

Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.

TokenStyles
--pct-breadcrumb-fg{pct.text}#0f172a · #ffffffText colour of the breadcrumb.
--pct-breadcrumb-font-size{pct.font.size.sm}13pxFont size of the breadcrumb.
--pct-breadcrumb-gap{pct.space.2}4pxGap between crumbs.
--pct-breadcrumb-link-fg{pct.text-muted}#334155 · #e2e8f0Text colour of a link.
--pct-breadcrumb-link-fg-hover{pct.text}#0f172a · #ffffffText colour of a link on hover.
--pct-breadcrumb-link-font-weight-current{pct.font.weight.medium}500Font weight of the link naming the current page.
--pct-breadcrumb-link-target-min{pct.target.min}24pxTouch-target floor of a link.
--pct-breadcrumb-separator-fg{pct.text-muted}#334155 · #e2e8f0Text colour of the separator.

Theming

A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.

[data-theme='brand'] {
  --pct-breadcrumb-link-fg: #0f766e;
  --pct-breadcrumb-link-fg-hover: #134e4a;
  --pct-breadcrumb-separator-fg: #5eead4;
}
Accessibility

Measured, not assumed

Pattern: Breadcrumb — (0054) a named navigation landmark, a list of links, aria-current="page" on the one you are on. Named in the class JSDoc.

Keyboard

keyeffecttest
Tab / Shift+Tabmoves between the stepsapps/sandbox-e2e/src/breadcrumb.spec.ts — "every step is a real tab stop"
Enterfollows the linknone — deliberately: the anchor navigates on its own, and testing it tests the browser (req-api-platform)

There are no handlers to map: nothing here listens to a key.

What is measured, and what is not yet

Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.

MeasuredARIA pattern named in the class JSDoclibs/components/breadcrumb/src/breadcrumb.ts
MeasuredKeyboard map tested key by keythe table above — one row measured, one deliberately the platform's
Measuredaxe audit on the component's own sandbox viewapps/sandbox-e2e/src/a11y.spec.ts — /breadcrumb in SBX_ROUTES; the audit put the target floor into the stylesheet (lesson-139)
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — breadcrumb-pattern and breadcrumb-pattern-rtl
Measuredforced-colors: active — no state carried by colour alonelibs/components/breadcrumb/src/link.scss — links LinkText, the current step a weight, the separator CanvasText. The reading: apps/sandbox-e2e/src/forced-colors.spec.ts
n/aprefers-reduced-motion — duration from a tokennot applicable — no motion at all
MeasuredTouch target ≥ 24×24 px outrightlibs/components/breadcrumb/src/link.scss — min-block-size: var(--pct-breadcrumb-link-target-min) on every link, measured in by the axe audit rather than believed
MeasuredSize axis aligned to --pct-control-height-*deliberately not: the trail is typography, not a control — the badge's argument one component over; it is sized by its own type token
GapDensity axisnone — gap: the same one every component here has (req-token-density)
MeasuredRTL — no physical properties + a dir="rtl" screenshotapps/sandbox-e2e/src/rtl.spec.ts — the trail mirrors and every separator turns (:dir(rtl), the calendar's selector). The screenshot: breadcrumb-pattern-rtl
MeasuredSSR + hydration with no NG05xxapps/sandbox-e2e/src/hydration.spec.ts — /breadcrumb in SBX_ROUTES
n/aFormsnot applicable — a trail holds no value a form owns
MeasuredParts registered in the inventorylibs/components/parts.snapshot.md — list, separator
MeasuredTokens registered + an entry in contrast.policy.jsonlibs/tokens/src/contrast.policy.json — four entries: both link states and the current word against the page (AA error), the separator's edge (UI warn — decorative, and exempt is no reason to stop measuring); libs/tokens/tokens.snapshot.md
MeasuredStrings through PCT_TEXTSlibs/components/core/src/texts.ts — breadcrumbLabel, read at render time
MeasuredEntrypoint size budgetlibs/components/size.snapshot.md — the ./breadcrumb row
MeasuredScreen-reader logRead 2026-09-16 from docs/acr/at/ — three readers, whole sandbox, and the current page is announced as current, by name: Data · link. · (Current page) (Orca), Data, link, current page (NVDA). The landmark and the list are announced on entry — navigation · Breadcrumb · List with 3 items (Orca) — so the trail is a list inside a named navigation to a reader, which is the whole promise. VoiceOver leaves this view after one stop: on 2026-09-16 it read the list on the first link, and in the pass of 2026-10-29 only its usage hint there
MeasuredA docs page with live examplesapps/sandbox/src/app/views/breadcrumb/ (the sandbox view). The published site: /components/breadcrumb — prerendered, the demo's own source is the code tab
MeasuredUnit + mutationlibs/components/breadcrumb/src/breadcrumb.spec.ts — 12 cases; libs/components/mutation.snapshot.md — breadcrumb.ts measured in the day's shared full run
Evidence

What the repository holds for this component

Read at build time from the tracked snapshots, not typed here.

92%mutation score23 of 25 mutants killed
6 × 3e2e cases × enginesplus the shared audits: axe, forced colors, preferences, size, hydration
4colour pairs measuredlight and dark, at every token build
2visual baselinesChromium, the sandbox's own pictures
17elements in the preview0 listeners · 1 render to settle · 5730 µs in jsdom, held exactly but for the clock

Known limitations

  • No collapse for long trails. A trail too long for its line wraps; folding steps into an overflow menu is a composition with pct-menu owned by the consumer, and it returns as its own decision the day a real application shows the need.
  • No items input. An array of {label, url} would regenerate anchors the router already owns — the markup is the consumer's, which is the component's founding split.
  • The component cannot verify the trail. A breadcrumb that lies about the hierarchy is the application's sentence; the component checks structure (the dev-mode warnings), never truth.
  • Bare-text current steps carry no weight. The weight rides on [aria-current], and bare text carries no attribute; it is told apart by colour in the author palette and by not-being-a-link in forced colours — a consumer who wants the weight writes the current step as a link to itself.