Skip to content
Monogem

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 itself aria-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 forwards href and every other prop straight through; there is no asChild. For client-side routing, render your router’s own link element inside the BreadcrumbItem with the same text-muted-foreground / hover:text-foreground treatment.
  • BreadcrumbPage — the page you are on. A non-interactive <span> marked aria-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 is aria-hidden; the sr-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

breadcrumb-showcase.tsx
"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

PartStateLook
BreadcrumbLinkResting--muted-foreground, no underline
BreadcrumbLinkHover--foreground + underline
BreadcrumbLinkFocus (keyboard)2px --ring focus ring, --radius-sm corners
BreadcrumbPageCurrent--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.

TokenWhere usedRationale
--muted-foregroundResting link text, separators, ellipsisColor names this for quiet secondary text; a breadcrumb is supporting, not primary
--foregroundCurrent page, link hoverThe one crumb that matters most (current) and the hover affordance get full-contrast text
--ringKeyboard focus ring on linksThe system-wide focus token — same ring as Button
--radius-smFocus-ring corner on linksInline text link, the smallest radius step
--scale-1-5 (gap-1.5, 6px)Gap between a crumb and its separatorA tight, even rhythm along the row — Spacing
--scale-4 (size-4, 16px)Separator + ellipsis glyph sizeIcons’ “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 + truncate on the BreadcrumbLink.

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 own aria-label to 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 ::before glyph would be. BreadcrumbEllipsis is not fully hidden: only its MoreHorizontal glyph carries aria-hidden, so the sr-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+Tab move between them, Enter activates. There is no roving tabindex — a breadcrumb is a list of links, not a composite widget.
  • Contrast: resting --muted-foreground on --background is the same pairing the rest of the system uses for secondary text — --neutral-600 on --neutral-50 in light (7.2:1) and --neutral-400 on --neutral-950 in dark, both clearing WCAG 2.1 AA for body text. Hover and current --foreground are 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.