Skip to content

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.

breadcrumb/basic.tsx

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.

breadcrumb/generated.tsx

Separators

separator on the root takes any node and every gap in the trail uses it.

breadcrumb/separators.tsx

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.

breadcrumb/variants.tsx
plain
chip

Sizes

breadcrumb/sizes.tsx

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.

breadcrumb/icons.tsx

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.

breadcrumb/collapsed.tsx

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

breadcrumb/overflow.tsx

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:

  • Root is 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.
  • List is 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.
  • Page is 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-current carries 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 the plain variant 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 plain variant is a colour, and forced colors flattens both to CanvasText; 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.

Theming tokens for Breadcrumb
PropertyControlsDefault
--forte-breadcrumb-gapSpace between a crumb and its separatorvar(--forte-space-2)
--forte-breadcrumb-row-gapSpace between rows of a wrapped trailvar(--forte-space-1)
--forte-breadcrumb-item-gapSpace between an icon and its label inside one crumbvar(--forte-control-gap)
--forte-breadcrumb-font-sizeText size of the whole trailvar(--forte-font-size-2)
--forte-breadcrumb-colorColour of an ancestor crumbvar(--forte-color-foreground-muted)
--forte-breadcrumb-color-hoverColour of an ancestor crumb under the pointervar(--forte-color-foreground)
--forte-breadcrumb-color-currentColour of the current page's crumbvar(--forte-color-foreground)
--forte-breadcrumb-font-weight-currentWeight of the current page's crumbvar(--forte-font-weight-medium)
--forte-breadcrumb-underline-offsetHow far a hover underline sits below the text0.2em
--forte-breadcrumb-separator-colorColour of the separatorvar(--forte-color-foreground-subtle)
--forte-breadcrumb-separator-sizeSize of the default chevron, relative to the trail's text1em
--forte-breadcrumb-ellipsis-pxInline padding of the ellipsis buttonvar(--forte-breadcrumb-chip-px)
--forte-breadcrumb-ellipsis-pyBlock padding of the ellipsis buttonvar(--forte-breadcrumb-chip-py)
--forte-breadcrumb-ellipsis-radiusCorner radius of the ellipsis buttonvar(--forte-radius-control)
--forte-breadcrumb-chip-pxInline padding of a chipvar(--forte-space-2)
--forte-breadcrumb-chip-pyBlock padding of a chip. Follows data-forte-densityvar(--forte-space-1)
--forte-breadcrumb-chip-radiusCorner radius of a chipvar(--forte-radius-control)
--forte-breadcrumb-chip-bg-hoverChip fill of an ancestor crumb under the pointervar(--forte-color-panel-hover)
--forte-breadcrumb-chip-bg-currentChip fill of the current page's crumbvar(--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

Props for BreadcrumbRoot
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
overflowBreadcrumbOverflowwrapWhat 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.
renderRenderProp<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.
separatorReactNodea chevronWhat 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.
sizeBreadcrumbSizemdText size, crumb padding and separator size for the whole trail.
variantBreadcrumbVariantplainHow 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.
Props for BreadcrumbList
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
separatorsbooleantrueInsert 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.
Props for BreadcrumbItem
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
Props for BreadcrumbLink
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
renderRenderProp<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.
Props for BreadcrumbPage
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
renderRenderProp<Record<string, unknown>>Replaces the rendered `<span>` with another element or component.
Props for BreadcrumbSeparator
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
Props for BreadcrumbEllipsis
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
labelstringMoreThe 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.
renderRenderProp<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.