1 slot open this monthBy the makers of shadcnregistryWe built shadcnregistry. Now we build your product.Book a callCheck availability
1 slot open this monthAI features?We build AI features into your product.Book a callCheck availability
1 slot open this month🚨 Slop police🚨 Slop police: we clean up vibe-coded apps.Report itReport your app
shadcnregistry
shadcnregistry
RegistryHealthInspect
Terms of ServicePrivacy Policy
  1. Registries
  2. pulld
  3. Breadcrumb Overflow

Breadcrumb Overflow

ui

A breadcrumb trail that measures itself and folds its middle into a menu when the row runs out of width, keeping the root you came from and the page you are on. Reach for it wherever a hierarchy has to survive a header nobody sized for it: an admin console's path, a file or storage browser, a category tree, a docs page's position, a deep settings screen, a project inside a workspace inside an organisation, a wizard's current step in a narrow panel, a dashboard that resizes with the window, a sidebar somebody can drag narrower, a trail that was two levels deep when it was designed and is five now. Common asks it answers: "collapsing breadcrumb react", "breadcrumb overflow shadcn", "breadcrumb with ellipsis in the middle", "truncate breadcrumb middle", "collapse breadcrumb items when they do not fit", "responsive breadcrumb react", "breadcrumb too long narrow screen", "breadcrumb wraps to two lines", "stop breadcrumb from wrapping", "breadcrumb dropdown for hidden levels", "shadcn BreadcrumbEllipsis dropdown", "how many breadcrumb items fit in a container", "measure available width react", "ResizeObserver breadcrumb", "ResizeObserver loop completed with undelivered notifications", "items appear then disappear on load", "flicker when hiding overflowing elements", "measure the natural width of a hidden element", "MUI Breadcrumbs maxItems equivalent shadcn", "itemsBeforeCollapse itemsAfterCollapse", "antd breadcrumb overflow", "aria-current page breadcrumb", "breadcrumb accessibility current page", "is the current breadcrumb a link", "breadcrumb nav aria-label", "focus lost when an element unmounts", "focus jumps to body when the focused link is hidden", "keyboard access to collapsed breadcrumb items", "rtl breadcrumb chevron direction", "breadcrumb separator rtl", "dropdown clipped by overflow hidden", "absolutely positioned menu not visible inside overflow hidden", "パンくず 折りたたみ react", "パンくずが2行になる", "パンくず 省略 真ん中", "長いパンくず 幅に収める", "入りきらない階層をメニューに畳む", "パンくず ドロップダウン", "aria-current 現在地", "現在地はリンクにしない", "ResizeObserver 無限ループ", "測ってから隠すとちらつく", "overflow hidden でメニューが見えない", "RTL パンくず 矢印の向き". What official shadcn/ui does instead is grow downwards, and that is a design decision rather than an omission: its `BreadcrumbList` is `flex flex-wrap`, so a trail that does not fit wraps onto another line. Measured on the same five-crumb path in Chrome today — one line at 480px, two lines and 45.2px tall at 320px, three lines and 70.8px at 240px, and zero horizontal overflow at every width. The header gets taller and everything below it moves down, which is fine for a page of prose and wrong for a toolbar row, a dialog title bar or a sticky header with a fixed height. The reflex fix is worse before it is better: `flex-nowrap` alone lets flex items shrink, and at 320px two of the five crumbs were squeezed until their own text wrapped — 39.2px tall against the 19.6px line — while the row still overflowed by 26px. So the crumbs here carry `shrink-0` and `whitespace-nowrap`, and something has to fold. Official has nothing that measures. Across its whole registry today — 65 items listed, 62 fetchable, `code-block`, `questionnaire` and `trace` indexed and 404 on both style tracks, 223,287 bytes of `files[].content` concatenated — `ResizeObserver`, `getBoundingClientRect`, `scrollWidth`, `offsetWidth` and `isTruncated` are every one of them a zero hit. Its `BreadcrumbEllipsis` is a static three-dot glyph you place yourself: `role="presentation"`, `aria-hidden="true"`, and an `sr-only` "More" **inside** that `aria-hidden` element, so the one label it carries is read by nothing, it is not a button, and nothing decides which levels it stands for. The two matches for `ellipsis` in the registry are that and `PaginationEllipsis`, and both are decoration. Measuring your own row is a loop unless you arrange otherwise. The implementation everybody writes first asks the live row how wide it is, hides what sticks out, and is asked again by its own ResizeObserver: the hidden crumbs now measure zero, so the trail fits, so they come back, so it overflows — the flicker behind "ResizeObserver loop completed with undelivered notifications", and no amount of debouncing fixes it, because every pass is a correct answer to a question that should not have been asked. Two things break it here. Natural widths come from a probe that is never folded — a hidden copy of the whole trail, absolutely positioned and `visibility: hidden` rather than `display: none` so it still has a box to measure, at `width: auto` and `max-width: none` so what it reports is the width each crumb wants. And the container's width cannot be changed by its contents: the list is a block-level, full-width box with `overflow-hidden`, so its width comes from its parent and a trail one pixel too wide is clipped rather than pushing anything wider. Together they make the fold a pure function of inputs that folding does not touch, so the same container width always produces the same answer and the observer settles after one pass. Because that answer is an object rather than a number, the bail-out React gives a state update to an equal value is kept by hand — the setter returns the previous object when both fields match, and without that one line every measurement is a render and the loop is back. The flash is a separate problem with a separate answer: the measurement runs in a layout effect, after layout and before paint, so a deep trail is never painted at full length and then collapsed. Which end gives way is the whole difference from every other overflow component. A toolbar keeps a prefix and folds the tail, because its first button is its most important one. A breadcrumb has to keep both ends — the root is how you get out and the current page is where you are — so what collapses is the middle, and the answer is how much of the tail survives with the root always in the row. Three pieces of arithmetic are easy to get wrong and each is written down in the source. A join between two crumbs costs `gap + separator + gap`, not one gap, because the list's gap applies on both sides of the separator: predicted against a real row, five crumbs came to 428.91px and measured 428.91px, and the folded shape predicted 211.05 and measured 211.05, while counting one gap per join under-reads a five-crumb trail by 24 pixels and keeps a level that does not fit. The ellipsis is charged for if and only if something folds — reserve room for it always and a trail that fits exactly loses a level to a control that was never needed; reserve it never and the level that "fits" is pushed out by the control added after the decision. And the fold never keeps `total - 1` trailing crumbs, which would be the whole trail behind a control hiding none of it, charging the row for itself and one more join. The menu cannot live in the list, and that is measured too. The list clips, which is what stops a trail far too narrow for its crumbs from making the page scroll sideways, and a panel absolutely positioned against the box that clips it is painted only where the two overlap: on this exact pair, a panel at `top-full` inside the clipping list was hit by 0 of 15 points scanned down its own rectangle, and the same panel moved onto the `<nav>` above it by 15 of 15. So the landmark positions the menu, the list clips, and the menu is placed under the ellipsis by arithmetic rather than by measuring it — the control is always the second box in the row, so its offset is the list's leading padding plus the root plus one join. Verified to the pixel in both writing directions: 68.25 predicted against 68.25 actual in LTR, 73.72 against 73.72 in RTL, with the panel landing on the control's inline edge in both. `inset-inline-start` counts from the inline start whichever way the text runs, so the sign flip that `scrollLeft` forces on an RTL page does not arise, and the default chevron mirrors itself rather than pointing back the way you came. The current page is not a link. It renders as a `<span aria-current="page">` — that attribute is the whole convention for "you are here", and official's own page element instead gives a span `role="link"` with `aria-disabled="true"`, which announces a dimmed link for something that is not one and was never going anywhere. It is also the one crumb allowed to shrink, `min-w-0` with CSS truncation, so a trail with no room left shortens the longest thing in it instead of being cut off mid-glyph; the probe keeps reporting that crumb's natural width while the live one is squeezed — measured at 225.44px natural against 15.75px on screen — so the truncation cannot feed back into the arithmetic. Every other per-part class a caller passes is on the probe's copy as well as on the row, because the probe measures what the row renders: `separatorClassName="px-2"` widens every join by four pixels, and a measurement that missed that would keep a level that does not fit. The keyboard is a breadcrumb's, not a toolbar's. The crumbs are links and each is an ordinary tab stop, because the practices give a breadcrumb no arrow keys and a roving tabindex here would take Tab's answer away from a reader. The ellipsis is the one control with keys of its own: Down or Up opens its menu on the first or last level, Escape closes it and returns focus, Tab leaves from an element still in the document, Home and End reach the ends, a horizontal arrow inside the menu is swallowed rather than left to scroll an ancestor, and the wrap is `split-button`'s own `nextMenuIndex`, imported rather than written again so the two menus in this registry cannot drift apart. One failure survives all of that and is handled on its own: a crumb holding focus can be folded away when the container narrows, and a browser does not move focus when the element holding it leaves the document — it drops to `<body>`, so the next Tab starts from the top of the page. The fold moves focus to the ellipsis, which is where that crumb now is, and only when focus was already in the trail, since a window being dragged narrower is not a reason to take focus from whatever somebody is doing elsewhere on the page. What you get: `BreadcrumbOverflow`, taking `items` root-first with the current page last, each `{ label, href?, icon?, id?, onNavigate? }` — `href` omitted for a level with no page of its own, which is text in the row and still a focusable rung in the menu, and `onNavigate` the hook for a client router (`event.preventDefault()` then `router.push`), which is also what tells the component the page is not being replaced so it puts focus back on the control instead of leaving it on a link that is about to unmount. `label` names the landmark ("Breadcrumb" by default), `separator` replaces the chevron in the row and in the probe alike, `labels.more(count)` builds the control's accessible name and carries the number of hidden levels because "More" alone does not say whether one level is hidden or nine, `onFoldChange` reports how many are behind the control after each measurement, and `onOpenChange` is told once per change rather than twice under strict mode. `fitBreadcrumbs` is the whole decision as a plain function of widths, available width, gap, separator and control width — the part worth asserting, since every interesting case here is a width rather than a click — and `breadcrumbMenuOffset` and `isCrumbFolded` are the placement and the fold membership, each on its own. Per-part `className` for the list, the crumbs, the current page, the separators, the control, the panel and its items. One file, two lucide icons, no `radix-ui`, and nothing portalled or measured against the viewport: the honest limits are the server, which cannot measure, so an SSR'd page shows the whole trail clipped until hydration, and an ancestor that scrolls, which can clip the panel. Distinct from its neighbours here: `toolbar-overflow` folds a row of actions into a menu at its end, keeping a prefix rather than both ends, and this borrows its probe discipline rather than its strategy; `middle-truncate` shortens one string from the middle so an extension survives, where this decides which whole levels survive; `overflow-tooltip` reveals a string that CSS has already clipped, which is the answer for the current page's title rather than for the trail; `scroll-shadow` says there is more past a scrolling edge instead of folding it; `sort-header`, `column-resizer` and `sticky-table-header` measure a table, while this measures itself; `split-button` is one action with its variants under an arrow, and the menu here composes with it.

pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/breadcrumb-overflow.json"
Premium blocks for shadcn/uiShadcnblocks.com · AffiliateExplore
  • lucide-react
  • split-button
components/ui/breadcrumb-overflow.tsx