Hierarchy-path navigation per the WAI-ARIA breadcrumb pattern: an ordered list of links from a root to the current page, inside a <nav> landmark labeled "Breadcrumb". Use it on detail pages inside deep hierarchies (an endpoint under a project under an account) so users can see how they got here and jump back up a level. It answers a different intent than Tabs, which switches what you're looking at within one place; a breadcrumb tells you where you are and offers the way up. It is router-agnostic: each Breadcrumb.Link is a plain <a> you can compose onto your router's link via asChild (see Polymorphism), and Anchor remains the right primitive for standalone external links.
Breadcrumb.Separator renders a caret icon by default; pass your own children to replace it (a slash, for instance):
Composition
Rootis the<nav>landmark, labeled"Breadcrumb"by default (overridable viaaria-label). It carries no visual styling of its own.Listis the<ol>: an ordered list, because the order of the items is the hierarchy, from the root to the current page. It lays the crumbs out in one row and scrolls them sideways when the trail is wider than its container (see Overflow).Itemis one crumb: an<li>wrapping aLink, aLabel, or thePage.Linkis a plain<a>to an ancestor page; compose it onto your router's link viaasChild.Labelis a crumb that is not a link: a level with no page of its own (see The prefix crumb). A<span>with noaria-current.Pagemarks the current page: a<span>witharia-current="page", not a link.Separatoris a purely visual divider between crumbs (role="presentation",aria-hidden), rendering a caret by default.Skeletonis the placeholder crumb for a label that is still loading (see The pending trail): one<li>as a crumb-sized pulsing bar with a screen-reader-only status announcement.
Polymorphism
Every part except Breadcrumb.Skeleton supports asChild: the skeleton's whole value is the placeholder content it generates, so there is no element to swap. The one that matters most is Breadcrumb.Link: the component never imports a router, so compose the link styling onto your framework's link (a react-router Link here) to get client-side navigation:
The prefix crumb
Not every crumb is a destination. In a trail like Settings > General, Settings is a section prefix: its index URL only redirects to /settings/general, so a link there would bounce the user back to the page they are already on. Render that crumb as a Breadcrumb.Label, muted text like the rest of the trail, with no href and no aria-current:
The label stays an Item in the <ol>, so it still counts as a step in the trail: assistive technology reads it as a crumb, without offering it as a link. To derive a trail like this from your route tree, see the prefix crumb in the breadcrumbs-from-routes recipe: a layout route contributes the label, and its child routes contribute the pages under it.
The pending trail
Some labels only exist in fetched data: Apps > my-app > Settings, where my-app is a resource name the URL does not carry. While that query is pending, Breadcrumb.Skeleton stands in for the unresolved segment: one crumb-sized pulsing bar, plus a screen-reader-only role="status" announcement ("Loading breadcrumbs…" by default; set label for localization). The bar is w-24 by default — pass a w-* utility for your best-guess width, so the row's shape holds when the real crumbs replace it:
The skeleton is one <li>, so the pending segment counts as one crumb until the real items arrive, and the status announcement is all assistive technology hears. One skeleton usually stands in for a whole segment; compose several with Breadcrumb.Separator between them when the segment's shape matters. The full pattern (a route contributes a component that shares the page's query and swaps the skeleton for real crumbs on resolve) is the query-backed crumb in the breadcrumbs-from-routes recipe.
Trails from route matches
@ngrok/mantle/breadcrumb also exports the route-trail model the breadcrumbs-from-routes recipe builds on: a route names itself in its handle export, and a pure builder turns the matched chain into crumbs. The exports are router-agnostic: buildCrumbs reads only id, pathname, and handle off each match, which react-router's UIMatch satisfies structurally.
routeBreadcrumbcreates the crumbs a route contributes: a link crumb by default,routeBreadcrumb.labelfor a prefix crumb that never links, androuteBreadcrumb.contentfor a segment that renders itself from fetched data (see The pending trail).buildCrumbsderives the resolved trail fromuseMatches();hasBreadcrumbis the type guard it filters with.BreadcrumbHandle<TMatch>types a route'shandleexport;CrumbandResolvedCrumbare the discriminated unions the factory and builder produce.
The recipe shows the whole thing wired together, including the one file an app copies: the renderer that owns your router's Link.
Overflow
An app header is one row tall, so a trail wider than that row scrolls sideways instead of wrapping onto a second one. Breadcrumb.List is the scroll container, and its edges carry the same scroll-fade-x mask as Tabs: the edge with crumbs beyond it fades out, and the edge with nothing beyond it stays flush.
The trail starts at its end, because the end is the current page. It scrolls itself there whenever that end moves — on mount, when a navigation swaps the crumbs, and when the row around it resizes — and leaves the reader's own scroll position alone the rest of the time, including while a crumb holds focus. Tabbing to a crumb the trail has scrolled past is the browser's own job; the list carries scroll-padding the width of the fade zone, so that crumb lands clear of the fade rather than under it.
For this to work inside a flex row such as AppLayout.Header, Breadcrumb.Root carries min-w-0: the landmark gives up width to the row instead of pushing its siblings out of it. The crumbs themselves never shrink, so a crumb keeps its label on one line.
Two constraints come with the mask, both covered in full on the scroll fade page:
- The mask fades everything
Breadcrumb.Listpaints, its own background and border included. Keep that styling on a wrapper, as the example above does. - The mask owns the element's
mask-image. A second mask on the same element replaces it.
To wrap instead, hand the scrollport back:
The mask needs a scroll container to animate against. Without one it stays fully opaque, and the row wraps as it did before.
Accessibility
Breadcrumb follows the APG breadcrumb pattern:
- The trail lives in a
<nav>landmark labeled"Breadcrumb", so screen reader users can jump straight to it. The label is overridable viaaria-labelonRoot; do so for localization, or to distinguish multiple breadcrumb trails on one page. - The crumbs are an
<ol>, so assistive technology announces the total count and each item's position; the order conveys the hierarchy. - The final crumb is a
Breadcrumb.Pagewitharia-current="page", announcing that it represents the current page. It is a<span>, not a link: linking to the page you are already on is a trap. - A
Breadcrumb.Labelcrumb is plain text: no link role, noaria-current. ItsItemstays in the<ol>, so screen readers count it as a step in the trail without offering it as a destination. - Separators are
role="presentation"andaria-hidden="true": purely visual, never counted as crumbs, never read aloud.
API Reference
Every part stamps a data-slot, joined after any chain a composing ancestor passes:
Breadcrumb.Root
The <nav> landmark containing the trail. It carries no visual styling; its one layout class is min-w-0, which lets the landmark shrink inside a flex row so Breadcrumb.List scrolls (see Overflow).
All props from nav, plus:
Breadcrumb.List
The ordered list of crumbs — the order is the hierarchy. It is also the scroll container: a trail wider than its container scrolls sideways with a faded edge and starts at the current page, and className="flex-wrap overflow-x-visible" trades that back for wrapping (see Overflow).
All props from ol, plus:
Breadcrumb.Item
A single crumb wrapping a Link, a Label, or the Page. The crumb never shrinks, so it keeps its label on one line and the trail scrolls instead.
All props from li, plus:
Breadcrumb.Link
A link to an ancestor page in the hierarchy.
All props from a, plus:
Breadcrumb.Label
A crumb that is not a link — a level with no page of its own, such as a section whose index URL only redirects (see The prefix crumb). It emits no aria-current and no link semantics.
All props from span, plus:
Breadcrumb.Page
The current page — the last crumb. Always emits aria-current="page".
All props from span, plus:
Breadcrumb.Separator
A purely visual divider between crumbs, hidden from assistive technology. Like the crumbs it divides, it never shrinks.
All props from li, plus:
Breadcrumb.Skeleton
A placeholder crumb for a label that is still loading (see The pending trail): one <li> as a crumb-sized pulsing bar, plus a screen-reader-only role="status" announcement. The bar is w-24 by default — pass a w-* utility in className for your best-guess width. It takes no children and no asChild — the part's whole value is the placeholder bar it renders.
All props from li except children, plus:
routeBreadcrumb
Creates the Crumb a route contributes (see Trails from route matches). The plain call creates a link crumb; two members cover the crumbs that are not plain links. There is no page variant on purpose — page-ness is positional, so the renderer draws the deepest crumb as Breadcrumb.Page.
buildCrumbs
Derives the trail — a ReadonlyArray<ResolvedCrumb> — from a matched route chain. Pure and router-agnostic: it reads only id, pathname, and handle off each match (the BreadcrumbMatch shape), which react-router's UIMatch satisfies structurally. Matches without a breadcrumb handle contribute nothing.
hasBreadcrumb
Whether a route match named itself in the trail — the type guard buildCrumbs filters with. Routers type handle as unknown, and the narrowing is what lets the builder read it without an assertion.
Crumb types
The types behind the factory and the builder, all exported from @ngrok/mantle/breadcrumb: