flyingsalmon

Breadcrumb

A single-line trail of the current page's ancestors in a hierarchy. Every level above the current one is a link back up, and the current page ends the trail as plain text.

A trail has at least two levels. A page that sits at the top of the hierarchy renders no breadcrumb, because a trail of one says nothing the page title has not already said.

Breadcrumb, sidebar, stepper, history

A breadcrumb shows location: where this page sits and what contains it. Sidebar moves between sections; breadcrumb moves up within one. Stepper is a position in a sequence a person is walking, which a hierarchy is not. And a breadcrumb is never history — it lists the pages above this one, not the pages you came through, so it reads the same however you arrived.

Parts

Four parts: Breadcrumb, BreadcrumbItem, BreadcrumbSeparator, and BreadcrumbEllipsis, with BreadcrumbEllipsisMenuItem for the levels the ellipsis hides. Breadcrumb is the nav and the ol inside it; every other part is an li.

One item, three shapes. BreadcrumbItem with link is an ancestor and renders an anchor; with active it is the page you are on and renders plain text; with neither it is a bare list item, which is what the ellipsis sits in. link and active are mutually exclusive, and the types say so.

You place every separator yourself. A chevron the list inserted on its own would land in the wrong place the moment a level is conditional or collapses into the ellipsis, so BreadcrumbSeparator is an item you write between two others. It holds a fixed chevron and there is no slot to change it.

There are no variants and no sizes. A breadcrumb sits in one place, above the page title, at text-sm.

A trail

Ancestors carry link and an href; the page you are on carries active, which renders plain text with aria-current="page". It is not a link and takes no focus, because a link to the page you are already on goes nowhere.

Light
Dark

Collapsing a long trail

A trail that does not fit collapses; it never wraps. Collapse from four levels: keep the root, the parent, and the current page visible, and hide everything between the root and the parent behind BreadcrumbEllipsis. Which levels collapse is your decision — the component measures nothing and collapses nothing on its own.

The ellipsis owns its menu. It renders the dropdown menu, its trigger, and its content, and its children are BreadcrumbEllipsisMenuItem, so a hidden level is always one click or one Arrow Down away. It goes in a BreadcrumbItem with neither link nor active. List the hidden levels from the highest ancestor down, the order they sit in the trail.

Light
Dark

A long page title

The current page is the part that truncates. It caps at 20 characters and shrinks below that cap when the row runs out of room; links never truncate, so the ancestors stay readable. Override the cap with className, and pass title for the full text.

The list clips nothing. A trail with more room than the container has overflows in view rather than disappearing at the edge, which is both the signal to collapse a level and the reason a focus ring is never cut off.

Light
Dark

Router links

A link item takes asChild and renders your router's link with breadcrumb's class names on it, so the trail navigates through the router instead of reloading the page. BreadcrumbEllipsisMenuItem takes the same asChild for the hidden levels. The trail below is this page's own: two real routes, which is the shortest trail worth rendering.

Light
Dark

States and motion

Links and the ellipsis trigger sit on --muted-foreground and step to --foreground on hover and while pressed, with no underline: position in the trail and the chevrons already say these are links, and indigo stays reserved for indicators. The current page is --foreground at normal weight. There is no disabled state.

The colour change is the only motion, on --motion-fast. The chevrons do not move, the trail has no enter animation, and truncation is not animated. The ellipsis menu opens and closes on the dropdown menu's own keyframes.

Accessibility

The root is a nav named “Breadcrumb”, which aria-label overrides, holding an ordered list. Separators are presentational and hidden, so a screen reader hears the levels and nothing between them. The current page is a span with aria-current="page" and no link role, so nothing in the trail announces as a link that goes nowhere.

Tab moves through the links and the ellipsis trigger and skips the current page. The trigger is named “Show hidden levels”, which its own aria-label overrides; Enter, Space, or Arrow Down opens its menu, arrows move through the hidden levels, and Escape closes it and returns focus to the trigger. Every focus ring stands 5px off the text inside the row's 6px gap, clear of the chevrons on either side.

Links and chevrons carry --muted-foreground on the page background at 4.73:1 in light mode and 7.63:1 in dark; hover and the current page are --foreground. All pass WCAG AA.