Breadcrumb
A breadcrumb is the line above a page that says where the page sits: Home › Settings › Billing. It is the shortest possible sitemap, drawn from the reader's position outward, and its whole job is to make the level above one click away.
Import
import { Breadcrumb } from "@forte-ui/react";Breadcrumb is a namespace of seven parts. The full anatomy, with every optional layer present:
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item>
<Breadcrumb.Link href="/">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Item>
<Breadcrumb.Ellipsis />
</Breadcrumb.Item>
<Breadcrumb.Item>
<Breadcrumb.Page>Billing</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>Most trails need less: Root → List → Item → Link, and a Page at the end.
Examples
From an array
The shape a real app writes. React.Children.toArray flattens what a .map() returns, so generated crumbs get their separators exactly like hand-written ones — and null children are dropped before the count is taken, which is what stops a conditionally rendered crumb from leaving a separator pointing at nothing.
Separators
separator on the root takes any node and every gap in the trail uses it.
The default chevron mirrors itself in RTL. A separator you pass does not — mirroring a glyph would turn a / into a \, so only the built-in icon consults --forte-direction. Flip the demo to RTL with the frame's direction control to see both halves of that.
Variants
plain is text with a hover underline: the trail above a document, sitting on the same rhythm as the prose. chip gives every crumb a rounded fill — a hover tint on the links and a resting one on the current page — which is what holds up when the trail sits on a toolbar rather than on the page background.
Sizes
The separator is sized in em, so it tracks the text through every preset instead of needing a scale of its own.
Icons
Crumbs are flex, so an svg goes in as a plain child. A direct-child svg is sized at 1em by the stylesheet and rides currentColor, so an icon follows the size preset and the hover colour without a class of its own.
An icon-only crumb still needs a name. The visually hidden span is what a screen reader reads; title is what a pointer user gets from the browser's own tooltip.
Collapsing a deep trail
Past four or five levels the trail is longer than the space above the page. Keep the first and last crumbs and fold the middle into a menu — Breadcrumb.Ellipsis is built to be that menu's trigger.
On its own, Ellipsis renders an inert <span>; the stylesheet keys its cursor off the element that actually got rendered, so it stays default as a span and becomes pointer as a button, with no prop to pass. Give it a label — three dots have no text of their own, and without one the control is announced as "button" and nothing else.
The inner render={<button type="button" />} is not decoration: Menu.Trigger styles itself as a button only when it is rendering its own default element, so handing it one keeps its chrome off the ellipsis.
Narrow screens
overflow="wrap" (the default) lets a long trail run onto a second line. overflow="scroll" keeps it on one line and Breadcrumb.List wraps itself in a ScrollArea, for the one thing a bare overflow-x: auto cannot do: say that it scrolls. Its edge fade is a mask driven off the remaining scroll distance, so it opens as the trail moves under it, closes flush at either end, and is right on any background. overscroll-behavior: contain comes with it, so a sideways swipe at the end does not drag the page along, as does a viewport that becomes focusable only while there is something to scroll.
There is no scrollbar. A scroll area's scrollbar overlays the content rather than insetting it, so on a one-line trail it is painted across the bottom of the crumbs — and reserving a strip to keep it off them would make every scrollable trail taller than every other one. The fade is the affordance here, and at this size it is the better one: it is there at rest rather than on hover, and it says which end still has trail on it.
Breadcrumb.List also scrolls it to the far end on mount: the end of a trail is the part a reader needs, and it is the part that falls off the edge. That only happens when the list actually overflows, so the wrap default never sees it, and it is keyed on the crumb count rather than on every render — a reader who scrolled back to the root crumb should not be yanked forward again by an unrelated re-render.
Routing
Framework links go in through render, which replaces the rendered <a> without losing the crumb's styling. With Next.js:
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Breadcrumb } from "@forte-ui/react";
export function Trail() {
const segments = usePathname().split("/").filter(Boolean);
return (
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item>
<Breadcrumb.Link render={<Link href="/" />}>Home</Breadcrumb.Link>
</Breadcrumb.Item>
{segments.map((segment, index) => {
const href = `/${segments.slice(0, index + 1).join("/")}`;
const last = index === segments.length - 1;
return (
<Breadcrumb.Item key={href}>
{last ? (
<Breadcrumb.Page>{label(segment)}</Breadcrumb.Page>
) : (
<Breadcrumb.Link render={<Link href={href} />}>
{label(segment)}
</Breadcrumb.Link>
)}
</Breadcrumb.Item>
);
})}
</Breadcrumb.List>
</Breadcrumb.Root>
);
}Deriving the trail from the URL like this is the cheap version, and it is wrong as often as it is right — a segment is an id or a slug at least as often as it is a title. Where the crumbs have real names, pass them down from the route's own data instead.
If the trail sits inside a <nav> you already own, swap the root's element the same way: render={<div />} on Breadcrumb.Root keeps the page to one landmark.
Accessibility
The keyboard story is the platform's: the crumbs are real links, so Tab walks them and Enter follows one. There is no roving focus to learn.
Everything else the component does by default:
Rootis a named landmark. It renders<nav aria-label="Breadcrumb">without being asked, because a page routinely carries several navigation landmarks and an unnamed one cannot be told from the others in a screen reader's landmark list. Override the label if the page carries two trails.Listis an<ol>. The trail's meaning is the order, and "list, 4 items" tells a reader how deep they are before they walk it.- Separators are
role="presentation" aria-hidden="true". They are punctuation; a screen reader reading "greater than" between every crumb is noise. The role also keeps them out of the item count, so the number announced is the number of crumbs. Pageis a<span aria-current="page">, not a link. A link to the page you are already on is a control that does nothing. The cue is never colour alone:aria-currentcarries it in the accessibility tree, and a weight change carries it on screen.- Crumbs meet the 24×24 target floor (SC 2.5.8) through
.forte-target, which grows the hit box without moving anything — so theplainvariant can keep the zero padding its look depends on. - Under forced colors, every link is underlined. The only thing separating an ancestor from the current page in the
plainvariant is a colour, and forced colors flattens both toCanvasText; the underline restores the distinction in the one channel that mode preserves.
Theming
Every property below is declared on Breadcrumb.Root, so override them there — through its className or an inline style — not on an ancestor, where the root's own declaration would beat the inherited value. The size presets work the same way: data-size="sm" and "lg" re-point a few of these knobs and nothing else.
| Property | Controls | Default |
|---|---|---|
--forte-breadcrumb-gap | Space between a crumb and its separator | var(--forte-space-2) |
--forte-breadcrumb-row-gap | Space between rows of a wrapped trail | var(--forte-space-1) |
--forte-breadcrumb-item-gap | Space between an icon and its label inside one crumb | var(--forte-control-gap) |
--forte-breadcrumb-font-size | Text size of the whole trail | var(--forte-font-size-2) |
--forte-breadcrumb-color | Colour of an ancestor crumb | var(--forte-color-foreground-muted) |
--forte-breadcrumb-color-hover | Colour of an ancestor crumb under the pointer | var(--forte-color-foreground) |
--forte-breadcrumb-color-current | Colour of the current page's crumb | var(--forte-color-foreground) |
--forte-breadcrumb-font-weight-current | Weight of the current page's crumb | var(--forte-font-weight-medium) |
--forte-breadcrumb-underline-offset | How far a hover underline sits below the text | 0.2em |
--forte-breadcrumb-separator-color | Colour of the separator | var(--forte-color-foreground-subtle) |
--forte-breadcrumb-separator-size | Size of the default chevron, relative to the trail's text | 1em |
--forte-breadcrumb-ellipsis-px | Inline padding of the ellipsis button | var(--forte-breadcrumb-chip-px) |
--forte-breadcrumb-ellipsis-py | Block padding of the ellipsis button | var(--forte-breadcrumb-chip-py) |
--forte-breadcrumb-ellipsis-radius | Corner radius of the ellipsis button | var(--forte-radius-control) |
--forte-breadcrumb-chip-px | Inline padding of a chip | var(--forte-space-2) |
--forte-breadcrumb-chip-py | Block padding of a chip. Follows data-forte-density | var(--forte-space-1) |
--forte-breadcrumb-chip-radius | Corner radius of a chip | var(--forte-radius-control) |
--forte-breadcrumb-chip-bg-hover | Chip fill of an ancestor crumb under the pointer | var(--forte-color-panel-hover) |
--forte-breadcrumb-chip-bg-current | Chip fill of the current page's crumb | var(--forte-color-panel-active) |
The parts also expose their state as data attributes — data-size, data-variant and data-overflow on the root — so a Tailwind arbitrary variant such as data-[variant=chip]:... can target them without a wrapper.
API reference
Breadcrumb.Root
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
overflow | BreadcrumbOverflow | wrap | What a trail too long for its container does. `"wrap"` lets it run onto a second line. `"scroll"` keeps it on one line in a horizontal scroller that starts scrolled to the current page — the end of the trail is the part a reader needs, and it is the part that falls off the edge. |
render | RenderProp<Record<string, unknown>> | Replaces the rendered `<nav>` with another element or component — pass `render={<div />}` when the trail sits inside a `<nav>` landmark you already own, so the page does not grow a second one. | |
separator | ReactNode | a chevron | What to draw between crumbs. Any node — a `/`, a `·`, an icon. Set once here and every gap in the trail uses it; a `Breadcrumb.Separator` written by hand with its own children still wins locally. |
size | BreadcrumbSize | md | Text size, crumb padding and separator size for the whole trail. |
variant | BreadcrumbVariant | plain | How loud the crumbs are. `"plain"` is text that underlines on hover. `"chip"` gives every crumb a rounded fill — a hover tint on the links and a resting one on the current page, which is what you want when the trail sits on a busy toolbar rather than above a document. |
Breadcrumb.List
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
separators | boolean | true | Insert a `Breadcrumb.Separator` between every pair of children. On by default — hand-interleaving separators is the step everyone forgets, and a trail is the one place where the count is always "one fewer than the items". Turning it off leaves the children exactly as written. Writing even one `Breadcrumb.Separator` by hand also turns it off for that list, so the two styles never double up. |
Breadcrumb.Item
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Breadcrumb.Link
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
render | RenderProp<Record<string, unknown>> | Replaces the rendered `<a>` with another element or component — `render={<Link href="/invoices" />}` is how a framework's router link goes in without losing the crumb's styling and states. |
Breadcrumb.Page
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
render | RenderProp<Record<string, unknown>> | Replaces the rendered `<span>` with another element or component. |
Breadcrumb.Separator
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Breadcrumb.Ellipsis
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
label | string | More | The accessible name, in a visually hidden span. Three dots have no text of their own, so without this the control is announced as "button" and nothing else. Pass `""` in the rare case where `children` is already text that names it, so the name is not said twice. |
render | RenderProp<Record<string, unknown>> | Replaces the rendered `<span>` with another element or component. Pass `render={<Menu.Trigger />}` to make the collapsed crumbs reachable — see the collapsed example in the docs. |