Skip to content
Primitiv home
Framework
Consumption mode

Breadcrumb Overflow

stableSource Figma

A Breadcrumb trail that collapses its middle entries behind an overflow menu once the trail exceeds keepStart + keepEnd items.

Playground

Density

Preview

Keep start
Keep end
Size
import { BreadcrumbOverflow } from "@/components/ui/breadcrumb-overflow";import { BreadcrumbLink, BreadcrumbPage } from "@/components/ui/breadcrumb";
<BreadcrumbOverflow keepStart={1} keepEnd={1} size="md">  <BreadcrumbLink href="#home">Home</BreadcrumbLink>  <BreadcrumbLink href="#library">Library</BreadcrumbLink>  <BreadcrumbLink href="#fiction">Fiction</BreadcrumbLink>  <BreadcrumbLink href="#mystery">Mystery</BreadcrumbLink>  <BreadcrumbPage>Neuromancer</BreadcrumbPage></BreadcrumbOverflow>

Density is set by a data-density ancestor — the Context system, not a BreadcrumbOverflow prop.

Installation

npx primitiv add breadcrumb-overflow

Import

import { BreadcrumbOverflow } from "@/components/ui/breadcrumb-overflow";

No headless primitive — this one ships only as a copied styled surface, so primitiv add is the only way in, whichever mode you are reading.

Props

BreadcrumbOverflow

Extends HTMLElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultDescription
childrenReactNode—The trail's BreadcrumbLink / BreadcrumbPage elements, in order.
keepEndnumber1How many crumbs to always show at the end of the trail (typically including the current page) after the overflow menu.
keepStartnumber1How many crumbs to always show at the start of the trail before the overflow menu.
menuLabelstring"Show hidden pages"Accessible label for the overflow trigger button.
size"xs" | "sm" | "md" | "lg" | "xl""md"Matches Breadcrumb's own size; also sizes the overflow trigger button and the menu it opens.

Styling contract

--primitiv-breadcrumb-overflow-trigger-hover-background--primitiv-breadcrumb-overflow-trigger-active-background

Keyboard

KeyBehaviour
EnterOpen the overflow menu when the … trigger is focused.
SpaceAlso opens the overflow menu — it is a real <button>.
ArrowDown / ArrowUpMove between the hidden crumbs once the menu is open (the Dropdown's roving focus).
EscapeClose the menu and return focus to the … trigger.

Accessibility

  • The hidden crumbs stay reachable. Unlike a bare Breadcrumb.Ellipsis (a decorative …), BreadcrumbOverflow's ellipsis is a real menu button opening a Dropdown of the collapsed crumbs — so every ancestor page is still navigable by keyboard and screen reader, not just by sight.
  • Name the overflow trigger. It is icon-only, so pass menuLabel (e.g. Show 3 hidden pages) for a name that says what the … does; a bare ellipsis announces nothing useful.
  • It is still a breadcrumb <nav>. The same landmark and aria-current="page" current-page rules as Breadcrumb apply — the last child should be a BreadcrumbPage, not a link.
  • Two on a page won't collide. Anchor positioning needs a unique anchor-name per instance; BreadcrumbOverflow derives its own from useId(), so you can render several without the menus fighting over one anchor — no wiring needed on your part.
  • Your crumb elements pass through untouched. The hidden ones are re-rendered inside the menu as-is, so an href, click handler or asChild router link on a BreadcrumbLink keeps working whether it shows in the trail or the overflow menu.

Examples

Collapsing a long trail

Pass the crumbs as flat BreadcrumbLinks ending in a BreadcrumbPage, and BreadcrumbOverflow keeps keepStart at the front and keepEnd at the back, folding the middle behind an ellipsis. Click the … — it opens a real menu of the hidden crumbs (a Dropdown), so nothing is lost. The anchor wiring is handled for you: it derives a unique anchor-name from useId(), so two of these on one page don't collide.

Density
import { BreadcrumbOverflow } from "@/components/ui/breadcrumb-overflow";import { BreadcrumbLink, BreadcrumbPage } from "@/components/ui/breadcrumb";
<BreadcrumbOverflow keepStart={1} keepEnd={1}>  <BreadcrumbLink href="#home">Home</BreadcrumbLink>  <BreadcrumbLink href="#library">Library</BreadcrumbLink>  <BreadcrumbLink href="#fiction">Fiction</BreadcrumbLink>  <BreadcrumbLink href="#mystery">Mystery</BreadcrumbLink>  <BreadcrumbPage>Neuromancer</BreadcrumbPage></BreadcrumbOverflow>

keepStart and keepEnd

keepStart / keepEnd are separate axes, not a single maxVisible, so you choose how many crumbs anchor each end independently — keep two at the front (Home / Library / … / Neuromancer) when the top of the hierarchy is the useful context, or two at the back when the leaf's neighbours are. The middle collapses only when more than one crumb would be hidden; otherwise every crumb shows.

Density
import { BreadcrumbOverflow } from "@/components/ui/breadcrumb-overflow";import { BreadcrumbLink, BreadcrumbPage } from "@/components/ui/breadcrumb";
<BreadcrumbOverflow keepStart={2} keepEnd={1}>  <BreadcrumbLink href="#home">Home</BreadcrumbLink>  <BreadcrumbLink href="#library">Library</BreadcrumbLink>  <BreadcrumbLink href="#fiction">Fiction</BreadcrumbLink>  <BreadcrumbLink href="#mystery">Mystery</BreadcrumbLink>  <BreadcrumbPage>Neuromancer</BreadcrumbPage></BreadcrumbOverflow>

Below the threshold

When the trail fits — keepStart + keepEnd covers it, with at most one crumb that would be hidden — nothing collapses and no menu appears: it renders every crumb exactly like a plain Breadcrumb. So you can wire BreadcrumbOverflow up unconditionally and it only reaches for the ellipsis when the trail actually outgrows the space.

Density
import { BreadcrumbOverflow } from "@/components/ui/breadcrumb-overflow";import { BreadcrumbLink, BreadcrumbPage } from "@/components/ui/breadcrumb";
{/* three crumbs, keepStart 1 + keepEnd 1 → nothing to collapse */}<BreadcrumbOverflow keepStart={1} keepEnd={1}>  <BreadcrumbLink href="#home">Home</BreadcrumbLink>  <BreadcrumbLink href="#library">Library</BreadcrumbLink>  <BreadcrumbPage>Neuromancer</BreadcrumbPage></BreadcrumbOverflow>

Sizes and density

size matches Breadcrumb's own scale and also sizes the overflow trigger and its menu, so the whole trail stays consistent. Each size rescales again with the nearest data-density ancestor.

Density
import { BreadcrumbOverflow } from "@/components/ui/breadcrumb-overflow";import { BreadcrumbLink, BreadcrumbPage } from "@/components/ui/breadcrumb";
<div data-density="comfortable">  <BreadcrumbOverflow size="sm" keepStart={1} keepEnd={1}>{/* … */}</BreadcrumbOverflow></div>