Breadcrumbs
Your you-are-here for Reference and Application pages — plus a quick hop to anything nearby. Each segment opens a popover of its siblings, so when the nav rail is tucked away (/app-bar), breadcrumbs pick up the sideways navigation for you.
BetaThis pattern ships as the Breadcrumbs component in @halo-compliance/ui — the trail above is that component, live. It is router-agnostic by design: deriving the trail from your nav registry stays on the consumer side, and the prop surface may still move.
Click any segment — its siblings pop up right there.
Behavior
Every segment is a popover trigger, not just a link. Click one and its siblings show up at that level — sibling sections for a section, sibling pages for the page you're on — with your current spot clearly marked. Pick a neighbor and off you go. Only one popover opens at a time; Escape or a click outside closes it and hands focus back to the segment.
One source, three surfaces
The trail comes from the same nav registry that powers ⌘K and the rail. Three ways to get around — rail (browse the map), breadcrumbs (step sideways), ⌘K (jump anywhere) — and they can never drift apart, because they share one source.
Props
- trail
- Root → current, in reading order. A segment with siblings renders as a popover trigger; one with only a `to` renders as a plain link (e.g. a Home root); the last carries `current`.
- onNavigate
- SPA navigation hook. Plain left-clicks call it; modifier / middle clicks keep native link behavior so "open in new tab" still works.
- onError
- Telemetry reporter for the internal boundary. The degraded fallback (current page as plain text) renders regardless — orientation is the one thing breadcrumbs must not lose.
Usage
import { Breadcrumbs, type BreadcrumbSegment } from '@halo-compliance/ui'
// Build the trail from the same nav registry that feeds the rail and ⌘K,
// so the three navigation surfaces cannot drift. Derivation stays on your
// side — the component is router-agnostic.
const trail: BreadcrumbSegment[] = [
{ label: 'Home', to: '/' }, // plain link (no siblings)
{ label: section.label, siblings: sectionPeers }, // popover of sibling sections
{ label: page.label, current: true, siblings: pagePeers },
]
<Breadcrumbs
trail={trail}
onNavigate={(to) => router.navigate({ to })} // SPA hop; modifier clicks stay native
onError={reportToTelemetry} // degraded fallback renders regardless
/>Accessibility
- Wrapped in a `<nav aria-label="Breadcrumb">` landmark over an ordered list.
- The current page carries `aria-current="page"` — in the trail and again in the popover.
- Each trigger sets `aria-expanded`; the popover is labelled by its trigger. Escape closes and restores focus.
- Separators are CSS-generated and out of the a11y tree — screen readers hear only the segments.
Anti-patterns
- Don't render the whole hierarchy. Breadcrumbs are the path to here, not a sitemap — that's the rail's job.
- Don't make the current segment navigate to itself. It opens its siblings like any other; the current one is flagged, not re-linked.
- Don't turn the popover into a big gold moment. Gold is for primary actions; the popover is a quiet helper — a flat, plain surface with a soft shadow, nothing more. (The hairline-frame, silver-register treatment belongs to the proposed system; here, flat and friendly is the rule.)
Related
- /app-bar — the disclosable rail-card whose dismissal makes breadcrumbs the primary lateral navigation (ADR-0031).
- ⌘K — jump-anywhere search; breadcrumbs are step-sideways. Same nav source.