Component · Navigation & Feedback
Breadcrumb
Overview
A secondary navigation trail that shows where the current page sits in the site hierarchy and lets the user jump straight back to any ancestor. It answers “where am I, and how do I step back up?” — it is an orientation aid, not the primary navigation, and it never replaces a page title.
Use it when pages nest more than one level deep and that nesting is stable and meaningful (docs, settings, catalog categories). Skip it for flat sites, for a linear flow (a checkout wizard — that’s a stepper), or when the hierarchy is only one level.
Breadcrumb ships as compound parts (like Tabs / Accordion) — the caller assembles the trail; the component supplies structure, semantics, and styling.
Anatomy
Breadcrumb <nav aria-label="Breadcrumb">
└─ BreadcrumbList <ol>
├─ BreadcrumbItem <li> → BreadcrumbLink (ancestor, <a>)
├─ BreadcrumbSeparator <li aria-hidden> (chevron, decorative)
├─ BreadcrumbItem <li> → BreadcrumbEllipsis (collapsed crumbs; glyph hidden, "More" announced)
├─ BreadcrumbSeparator
└─ BreadcrumbItem <li> → BreadcrumbPage (current page, <span>, not a link)
Breadcrumb— the<nav>landmark. It labels itselfaria-label="Breadcrumb"so screen readers can tell it apart from the primary nav.BreadcrumbList— an ordered list (<ol>). Position in the trail carries meaning, so the list is ordered, not a plain row.BreadcrumbItem— one crumb’s<li>wrapper.BreadcrumbLink— a link to an ancestor page. A native<a>that forwardshrefand every other prop straight through; there is noasChild. For client-side routing, render your router’s own link element inside theBreadcrumbItemwith the sametext-muted-foreground/hover:text-foregroundtreatment.BreadcrumbPage— the page you are on. A non-interactive<span>markedaria-current="page"— there is nowhere for it to navigate to.BreadcrumbSeparator— the glyph between crumbs. Decorative and out of the accessibility tree. Default is a right chevron; pass any node as its child to change it (e.g. a/).BreadcrumbEllipsis— a “…” standing in for crumbs the caller has left out on a narrow viewport. The glyph isaria-hidden; thesr-only“More” beside it is announced (it sits outside any hidden subtree), so a screen-reader user knows the trail was shortened. It does not expand.
Live Example
Full trail
Collapsed middle (caller-driven)
Custom separator
Code Example
"use client";
import type { MouseEvent } from "react";
import { Slash } from "lucide-react";
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
/**
* Live Breadcrumb example — `"use client"` only so the demo can swallow the
* click; the Breadcrumb parts themselves are server components. Ancestor items
* carry real `href` values (so hover, focus ring, and the pointer cursor are
* the genuine link behaviour), and `preventDefault()` stops the demo from
* navigating to a route the docs site doesn't have. Shows a full trail with the
* current page as a non-link `BreadcrumbPage`, a caller-collapsed trail using
* `BreadcrumbEllipsis`, and a custom `/` separator. Which crumbs collapse is
* the caller's choice — the component does no measurement.
*/
const stayOnPage = (event: MouseEvent<HTMLAnchorElement>) =>
event.preventDefault();
export function BreadcrumbShowcase() {
return (
<div className="flex flex-col gap-8">
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Full trail
</h3>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/" onClick={stayOnPage}>
Home
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/docs" onClick={stayOnPage}>
Docs
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/docs/components" onClick={stayOnPage}>
Components
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Collapsed middle (caller-driven)
</h3>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/" onClick={stayOnPage}>
Home
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbEllipsis />
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/docs/components" onClick={stayOnPage}>
Components
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Pagination</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Custom separator
</h3>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/" onClick={stayOnPage}>
Home
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<Slash />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink href="/settings" onClick={stayOnPage}>
Settings
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<Slash />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbPage>Profile</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</section>
</div>
);
}Variants
None. One visual style. The only swap is the separator glyph (chevron by default; a / is
the common alternative), passed as the child of BreadcrumbSeparator. No size, colour, or
emphasis variant — a breadcrumb is quiet supporting text by definition.
States
| Part | State | Look |
|---|---|---|
BreadcrumbLink | Resting | --muted-foreground, no underline |
BreadcrumbLink | Hover | --foreground + underline |
BreadcrumbLink | Focus (keyboard) | 2px --ring focus ring, --radius-sm corners |
BreadcrumbPage | Current | --foreground, font-normal, not focusable, not a link |
BreadcrumbSeparator / BreadcrumbEllipsis | — | Decorative, --muted-foreground, no states |
There is no disabled state. An ancestor that genuinely can’t be linked (a grouping label
with no page of its own) is written as plain text in a BreadcrumbItem, styled like
BreadcrumbPage but without aria-current.
Usage Guidance
Tokens
Breadcrumb uses existing Monogem semantic and primitive tokens to stay consistent with the design system; it introduces no component-specific styling values.
| Token | Where used | Rationale |
|---|---|---|
--muted-foreground | Resting link text, separators, ellipsis | Color names this for quiet secondary text; a breadcrumb is supporting, not primary |
--foreground | Current page, link hover | The one crumb that matters most (current) and the hover affordance get full-contrast text |
--ring | Keyboard focus ring on links | The system-wide focus token — same ring as Button |
--radius-sm | Focus-ring corner on links | Inline text link, the smallest radius step |
--scale-1-5 (gap-1.5, 6px) | Gap between a crumb and its separator | A tight, even rhythm along the row — Spacing |
--scale-4 (size-4, 16px) | Separator + ellipsis glyph size | Icons’ “inline with text” size, matching Button’s inline-icon size |
Text recipe: text-sm leading-snug — the system’s small-text setting.
Do / Don’t
Do
- Start the trail at a stable root (
Home, or the section root) and end it at the current page. - Mark the last item with
BreadcrumbPage— it is the current location, not a link. - Keep labels short and match them to the destination page’s title.
- Collapse the middle of a long trail yourself: render the first crumb, a
BreadcrumbEllipsis, then the last one or two. Always keep the root and the current page visible. - Truncate a single over-long label with a
max-width+truncateon theBreadcrumbLink.
Don’t
- Use a breadcrumb as the only navigation on a page — it supplements primary nav, it doesn’t replace it.
- Put the current page in an
<a>or make it clickable “to refresh”. - Build a breadcrumb for a linear multi-step flow — that’s a progress stepper.
- Include every query-string state or filter as a crumb; a breadcrumb reflects page hierarchy, not history.
- Rely on the separator to carry meaning — it’s decorative and hidden from assistive tech.
Accessibility
- The trail is wrapped in
<nav aria-label="Breadcrumb">, so it is exposed as a distinct navigation landmark. The label defaults to “Breadcrumb” and is applied after any spread props so it can’t be broken by accident; pass your ownaria-labelto name a specific trail when a page carries more than one. - The list is an
<ol>— order is meaningful, and screen readers announce position (“2 of 4”). - The current page is a
<span aria-current="page">, not a link: it is announced as the current item and is not in the tab order. (WAI-ARIA APG breadcrumb pattern.) - Separators are
<li role="presentation" aria-hidden="true">— removed from the accessibility tree entirely, the way a CSS::beforeglyph would be.BreadcrumbEllipsisis not fully hidden: only itsMoreHorizontalglyph carriesaria-hidden, so thesr-only“More” next to it is announced — the collapsed crumbs aren’t in the DOM, so without it a screen-reader user would have no signal the trail was shortened. - Keyboard: nothing custom. Links are in natural tab order;
Tab/Shift+Tabmove between them,Enteractivates. There is no roving tabindex — a breadcrumb is a list of links, not a composite widget. - Contrast: resting
--muted-foregroundon--backgroundis the same pairing the rest of the system uses for secondary text —--neutral-600on--neutral-50in light (7.2:1) and--neutral-400on--neutral-950in dark, both clearing WCAG 2.1 AA for body text. Hover and current--foregroundare near-maximal contrast in both themes. The focus ring is the system--ring, unchanged.
Related Components / Patterns
- Navigation — how Breadcrumb coexists with Navbar, Sidebar Nav, and Tabs.