Breadcrumb
A hierarchical navigation trail — links, separators, the current page, and ellipsis collapse for long paths.
- Status
- Since
0.1.0- Accessibility pattern
- APG breadcrumb
Last updated
Install
Add Breadcrumb from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/breadcrumbThe same command installs the registry items it composes: @vegastack/dropdown-menu.
Usage
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Settings</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>;Anatomy
Breadcrumb is a compound component built from a presentational <nav aria-label="breadcrumb">
wrapping an ordered list. Compose the parts inside the root — every part exposes a data-slot:
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbEllipsis /> {/* collapse long trails */}
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Current page</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>Breadcrumb— the navigation landmark (data-slot="breadcrumb"). Renders<nav aria-label="breadcrumb">.BreadcrumbList— the ordered list (<ol>,data-slot="breadcrumb-list"). Muted, wrapping, flex-aligned.flex-wrapis the fallback when nothing collapses the trail.BreadcrumbItem— a single trail segment (<li>,data-slot="breadcrumb-item").BreadcrumbLink— a navigable segment (<a>,data-slot="breadcrumb-link"). Supports the Base UIrenderprop for client-side routing.BreadcrumbPage— the current, non-navigable page (data-slot="breadcrumb-page",aria-current="page").BreadcrumbSeparator— the divider between items (data-slot="breadcrumb-separator",aria-hidden). Defaults to a chevron; passchildrento override.BreadcrumbEllipsis— a decorative collapsed-segments indicator (data-slot="breadcrumb-ellipsis",aria-hidden) for long paths. Pair it with a real trigger (seeBreadcrumbCollapsedbelow) when the collapsed segments need to be reachable.BreadcrumbCollapsed— a real, navigable stand-in for a run of collapsed segments: theBreadcrumbEllipsisglyph behind a labelledDropdownMenutrigger (data-slot="breadcrumb-collapsed-trigger"), so hidden segments stay reachable by keyboard and assistive tech. Each item is a real link.BreadcrumbTrail— the ergonomicitems+maxItemsAPI: pass the full trail and it wires upBreadcrumbCollapsedfor you. Renders aBreadcrumbList.
Examples
Custom separator
BreadcrumbSeparator renders a lucide-react chevron by default. Pass children to swap it for any
custom divider — here a slash. The separator stays decorative (aria-hidden) whatever you put inside:
<BreadcrumbSeparator>/</BreadcrumbSeparator>Ellipsis collapse
Collapse the middle of a long trail with a decorative BreadcrumbEllipsis:
Ellipsis with a menu
BreadcrumbEllipsis is decorative on its own. When the collapsed segments need to be reachable, wrap
it in a labelled menu trigger (here a DropdownMenu) so the hidden links stay navigable by keyboard and
assistive tech:
Collapsed segments (BreadcrumbCollapsed)
BreadcrumbCollapsed formalizes the manual composition above into one piece — pass the hidden
segments as items (each a real href, or a render-composed router link) and it renders the
BreadcrumbEllipsis glyph behind a labelled DropdownMenu trigger for you:
<BreadcrumbItem>
<BreadcrumbCollapsed
items={[
{ label: "Workspace", href: "/w" },
{ label: "Projects", href: "/w/p" },
]}
/>
</BreadcrumbItem>Responsive trail (BreadcrumbTrail)
For a long trail built from data, BreadcrumbTrail is the primary, SSR-safe ergonomic API: pass
the full items array and a maxItems — the first item and the last itemsAfterCollapse items
(default 1) always stay visible, and everything between collapses behind BreadcrumbCollapsed.
Collapsing is computed from items.length alone (no measurement), so it's deterministic on the
server and the client and never re-flows after hydration. Omit maxItems and the trail falls back
to BreadcrumbList's flex-wrap — nothing collapses:
<Breadcrumb>
<BreadcrumbTrail
items={[
{ label: "Home", href: "/" },
{ label: "Workspace", href: "/w" },
{ label: "Projects", href: "/w/p" },
{ label: "Settings", href: "/w/p/settings" },
{ label: "Billing" },
]}
maxItems={4}
/>
</Breadcrumb>A width-driven dynamic collapse (mirroring TruncatedText's ResizeObserver measurer) was
evaluated and deliberately deferred — measuring a set of items (not one text node) needs a
resize-and-recompute loop that tries collapse states until the trail fits, which risks visible
layout thrash and SSR/hydration mismatches for a component that sits at the very top of the page.
An honest, deterministic maxItems beats a flaky measurer; drop maxItems at a smaller value on
narrow breakpoints (e.g. via a responsive className) if you need the trail to shrink.
Routing links
Use the render prop on BreadcrumbLink to compose with your router's link component while keeping
breadcrumb styling and semantics:
import Link from "next/link";
<BreadcrumbLink render={<Link href="/settings" />}>Settings</BreadcrumbLink>;API Reference
BreadcrumbLink
| Prop | Type | Default | Description |
|---|---|---|---|
render | useRender.RenderProp<Record<string, unknown>> | — | Replace the rendered <a> element via Base UI render composition. Pass a
routing link element (e.g. <NextLink href="/x" />) or a render function to
integrate with a router while keeping breadcrumb styling. |
BreadcrumbCollapsed
| Prop | Type | Default | Description |
|---|---|---|---|
items* | BreadcrumbSegment[] | — | The hidden middle segments, revealed as real links inside a menu. |
label | string | 'Show hidden breadcrumbs' | Accessible name for the menu trigger — the collapsed run has no visible text of its own, so this is what assistive tech announces. |
Data attributes and CSS variables on BreadcrumbCollapsed
| Attribute | Values |
|---|---|
data-slot | "breadcrumb-collapsed-trigger" |
BreadcrumbTrail
| Prop | Type | Default | Description |
|---|---|---|---|
items* | BreadcrumbSegment[] | — | The full trail, first to last. Every item renders as a BreadcrumbLink
except the last, which always renders as the current, non-navigable
BreadcrumbPage — matching the manual composition pattern above. |
collapsedLabel | string | — | Accessible name for the collapsed-segments menu trigger. |
itemsAfterCollapse | number | 1 | How many trailing items (counting the current page) stay visible when
maxItems triggers a collapse. Only read while collapsing. |
maxItems | number | — | Collapse the middle of the trail once items.length exceeds this count:
the first item and the last itemsAfterCollapse items stay visible, and
everything between collapses behind a BreadcrumbCollapsed menu.
Omit (default) to never collapse — the list falls back to
BreadcrumbList's flex-wrap, same as manual composition.
Purely a function of items.length, so it is SSR-safe and deterministic
on first paint — no measurement, no post-hydration re-flow. A
width-driven dynamic collapse (mirroring TruncatedText's
ResizeObserver measurer) was evaluated and deliberately deferred; see
the component doc. |
BreadcrumbSegment
The item shape consumed by BreadcrumbCollapsed and BreadcrumbTrail:
| Prop | Type | Default | Description |
|---|---|---|---|
label* | React.ReactNode | — | The segment's visible label. |
href | string | — | Href for a plain <a> link. Omit and pass render for router composition instead. |
key | React.Key | — | React key, when the array index isn't stable enough (e.g. reordering). |
render | useRender.RenderProp<any> | — | Replace the rendered link element via Base UI render composition (e.g. a
NextLink). Takes precedence over href when both are set.
Typed <any> (not the default Record<string, unknown> state) so the same
segment feeds either BreadcrumbLink's render (no state) or
DropdownMenuItem's render (a { disabled, highlighted } state) inside
BreadcrumbCollapsed — the two Base UI callback shapes are otherwise
not mutually assignable. |
Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbPage,
BreadcrumbSeparator, and BreadcrumbEllipsis add no props of their own —
each accepts everything its underlying native element accepts (<nav>, <ol>,
<li>, <span>), plus className and ref.
Accessibility
- The root is a navigation landmark:
<nav aria-label="breadcrumb">, so assistive tech announces and lists it distinctly from other<nav>regions. - Segments live in an ordered list (
<ol>/<li>), conveying the hierarchy and position to screen readers. - The current page uses
aria-current="page"(and is not a link) so it is announced as the active location. - Separators are decorative:
aria-hidden="true"+role="presentation", so they are skipped by screen readers. BreadcrumbEllipsisis decorative (aria-hidden="true"). Pair it with a separate labelled control (for example, a menu trigger) when the collapsed segments need to be reachable.BreadcrumbCollapsed(andBreadcrumbTrail'smaxItemscollapse) is that labelled control: anaria-label'd menu trigger (default"Show hidden breadcrumbs", override withlabel/collapsedLabel) that opens aDropdownMenu. Every hidden segment inside it is a real link (<a href>or arender-composed router link) — not a JS-only action — so middle-click, "open in new tab", and crawlers keep working.- Links use the native
<a>and keep a visible:focus-visiblering — neveroutline: none.
| Key | Action |
|---|---|
| Tab | Move focus through the breadcrumb links. |
| Enter | Follow the focused link. |
| Contract | States tested |
|---|---|
| Behaviour | default, collapsed, disabled, open |
| Accessibility | current, disabled, focus-visible, labeled, semantic-html |
| Visual | default, hover, focus |