Navigation
Breadcrumbs
An accessible hierarchical breadcrumb trail with collapsible overflow — keeps deep paths short until the consumer asks to see the middle.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/breadcrumbs.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "Breadcrumbs" component using
lucide-react for icons.
Contract
- export const Breadcrumbs = React.forwardRef<HTMLElement, BreadcrumbsProps>
rendering a <nav>; spread remaining native nav props on the root, merge
className via cn().
- items: { label: string; href?: string }[] — the last entry is the current
page and must omit href; it renders as inert text with aria-current="page"
instead of a link. Earlier entries with href render real <a href>; the
consumer supplies actual app routes.
- maxItems?: number (default 0 = never collapse). When maxItems > 0 and
items.length > maxItems, collapse to: first item + an ellipsis + the last
(maxItems - 1) items.
- separator?: React.ReactNode (default a lucide ChevronRight, size-3.5),
rendered between every visible crumb and right after the ellipsis.
Behavior
- Semantic structure follows the WAI-ARIA breadcrumb pattern: <nav
aria-label="Breadcrumb"> wrapping a single <ol>; each crumb and each
separator is its own <li>. Separator <li>s carry aria-hidden="true" and
role="presentation" so screen readers announce only the crumb labels.
- The collapsed ellipsis is a real focusable <button type="button"
aria-label="Show hidden items"> — never decorative text. Clicking it
flips one internal boolean (expanded) to true, re-rendering the full,
uncollapsed trail. This is a one-way disclosure: there's no control to
re-collapse, matching how most breadcrumb overflow menus behave.
- No custom keyboard handling is needed beyond the browser's native tab
order — crumbs are plain anchors/button, so Tab/Shift+Tab and Enter/Space
already work.
Rendering & styling
- Semantic tokens only. Container text-sm. Links: text-muted-foreground,
hover:text-foreground, transition-colors. Current page: text-foreground
font-medium, no hover state (it isn't interactive). Ellipsis button:
text-muted-foreground, hover:bg-muted hover:text-foreground,
focus-visible:ring-2 ring-ring, sized as a small square (size-6) so it
reads as a control, not stray punctuation.
- The <ol> is flex flex-wrap items-center gap-1.5 so a long trail wraps to a
second line instead of overflowing — collapsing via maxItems is the
preferred fix for width, wrapping is just the safety net.
- No motion in this component; nothing here is decorative animation, so
there's no prefers-reduced-motion branch to write.
Customization levers
- Separator: swap the ChevronRight for a Slash, a dot, or plain text like
"/" — anything passed via the separator prop, no internal changes needed.
- Long last-segment truncation: add max-w-[Npx] truncate to the
aria-current="page" span for trails whose final label can be arbitrarily
long (file names, product titles).
- Structured data: because labels stay plain strings, a page can walk the
same items array to emit a BreadcrumbList JSON-LD <script> in the page
head for SEO — that's a page-level concern, not something the component
does itself.
- Router integration: the default renders plain <a href>; swap that branch
for next/link's Link (or another router's Link) to get client-side
transitions and active-route awareness, without touching the collapse or
accessibility logic.
- Collapse threshold: maxItems is the only sizing knob — raise it for wide
headers, lower it (e.g. 3) for narrow sidebars or mobile toolbars.Concepts
- APG breadcrumb pattern —
nav[aria-label="Breadcrumb"]wraps a singleol; the current page is inert text witharia-current="page", never a link, so assistive tech doesn't offer to "navigate" to where the user already is. - Collapsed overflow ellipsis — a real, focusable button rather than decorative "…" text: it has an accessible name ("Show hidden items") and reveals the hidden middle segments on demand instead of silently dropping them.
- One-way disclosure — expanding is a one-time state flip with no path back to collapsed; this mirrors how most breadcrumb overflow menus behave and keeps the interaction model simple.
- Presentational separators — chevrons (or any custom node) live in their own
aria-hiddenlist items, so screen readers announce a clean sequence of crumb names with no stray glyphs between them. - Consumer-owned links — crumbs render plain
<a href>; there's no internal router dependency, so swapping innext/link'sLinkor another router's link component is a same-shape edit, not a rewrite.