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. Toolbar Overflow

Toolbar Overflow

ui

A row of controls that folds whatever will not fit into a menu at its end — the pattern usually called priority+. Reach for it wherever a row of buttons has to survive a width nobody chose: an editor's formatting bar, a table's action column, an article's share row, an admin header, a card's footer actions, a navigation bar that has dropped to a phone, a sidebar somebody can drag narrower, a dashboard panel that resizes with the window, a toolbar that grew one button at a time until it stopped fitting on a laptop. Common asks it answers: "priority plus pattern react", "overflow menu react", "toolbar overflow shadcn", "responsive toolbar that collapses buttons", "move buttons into a dropdown when they do not fit", "collapse toolbar items into a more menu", "hide toolbar buttons on narrow screens", "react overflow set", "fluent ui overflowset equivalent shadcn", "material toolbar overflow shadcn", "three dot menu for the extra actions", "kebab menu for overflow items", "how many items fit in a container react", "measure available width react", "ResizeObserver toolbar react", "hide overflowing children with ResizeObserver", "detect overflowing flex items react", "ResizeObserver loop completed with undelivered notifications", "ResizeObserver infinite loop when hiding elements", "toolbar flickers while resizing", "items appear then disappear on load", "buttons flash before collapsing on first render", "hidden items measure zero width", "measure the natural width of a hidden element react", "how to measure an element that is display none", "overflow button width not accounted for", "last item overflows after the more button is added", "off by one gap in a flex overflow calculation", "flex items shrink instead of overflowing", "flex-shrink-0 buttons overflow", "role toolbar arrow keys react", "roving tabindex toolbar", "aria toolbar keyboard navigation", "focus lost when an element unmounts", "focus jumps to body when the focused button is hidden", "tab skips my toolbar", "no tab stop left in a roving tabindex group", "arrow keys stop working after a resize", "overflow menu accessible name", "aria-haspopup menu button react", "escape should close the overflow menu", "rtl toolbar arrow keys reversed", "icon only toolbar button accessible name", "ツールバー 折りたたみ react", "入りきらないボタンをメニューに畳む", "その他メニュー 実装", "レスポンシブ ツールバー ボタン", "幅に応じてボタンを隠す", "ResizeObserver 無限ループ", "測ってから隠すとちらつく", "role=toolbar 矢印キー", "roving tabindex フォーカスが飛ぶ", "三点メニュー 余りの操作", "フォーカスしていた項目が隠れると Tab が飛ぶ". Official shadcn/ui ships both halves of this and nothing that measures. Measured across its whole registry today — 63 items listed, 62 fetchable, `questionnaire` indexed and 404 on both style tracks, 223,287 bytes of `files[].content` concatenated, the fifth consecutive day at that exact figure — `ResizeObserver`, `getBoundingClientRect`, `offsetWidth`, `IntersectionObserver`, `requestAnimationFrame`, `overflowItems`, `priorityPlus`, `roving`, `toolbar` and `role="toolbar"` are every one of them a zero hit. There is one `aria-orientation` in the whole registry and it is not an attribute: it is a Tailwind variant selector, `aria-[orientation=horizontal]:w-full` and five of its siblings, styling the handle in `resizable` (1,805 bytes) whose actual attribute comes from `react-resizable-panels`, an npm dependency — the same shape as the single `aria-selected` that turns out to be a `calendar` variant. So the registry contains no file that measures an element and no file that writes a toolbar's keyboard, and the two pieces it does ship are the two halves of the problem: `button-group` (2,203 bytes, `cn` and nothing else) joins the buttons visually, and `dropdown-menu` (8,408 bytes) is the menu, handed to `radix-ui` as an npm dependency — which is also why grepping official for `aria-haspopup`, `role="menu"` or `menuitem` proves nothing in either direction, since Radix supplies them at runtime. `menubar` (8,379) and `navigation-menu` (6,648) are Radix wrappers too, and both are navigation rather than folding. Nothing in the registry has a route to "how many of these fit", so everybody writes that part by hand, and **measuring your own row is a loop unless you arrange otherwise.** The reflex implementation asks the live row how wide it is, hides what sticks out, and is then asked again by its own ResizeObserver: the hidden controls now measure zero, so the row fits, so they come back, so it overflows, so they are hidden — and because every pass changes layout, the observer fires again. That is 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 full set, absolutely positioned and `visibility: hidden` rather than `display: none` so it still has a box to measure, on one line with `width: auto` and `max-width: none` so what it reports is the width each control wants rather than the width it was given. And the container's width cannot be changed by its contents: the root is a block-level, full-width box with `overflow-hidden`, so its width comes from its parent and a row one pixel too wide is clipped rather than pushing anything wider. Together they make the visible count a pure function of inputs that folding does not touch, so the same container width always produces the same count, React bails out of a state update to an equal value, and the observer settles after one pass — the loop is removed rather than damped. The flash is a separate problem: the measurement runs in a layout effect, after layout and before paint, so the full row is never painted and then collapsed. The honest limit is the server, which cannot measure, so an SSR'd page shows the row clipped until hydration rather than wrapped or overflowing. Four more things are easy to get wrong and are each written down in the source. The trigger's own width is charged if and only if something folds: reserve it always and a toolbar whose controls fit exactly loses its last one to a button that was never needed, reserve it never and the control that "fits" is pushed out by the trigger added after the decision, so the row overflows by exactly the width of the thing meant to prevent overflow. The gaps go with it — `n` controls alone have `n - 1` gaps, `k` controls plus a trigger have `k`, because the gap in front of the trigger is real. The controls carry `shrink-0`, which is load-bearing rather than cosmetic: a flex item that may shrink does, so without it the browser squeezes everything into unreadable slivers, the row never overflows, and the component silently does nothing. And when the row narrows, two different things go wrong with the tab stop, only one of which is about focus — the visible failure is that the control holding focus leaves the document and a browser does not move focus when that happens, it drops to `<body>`, so the next Tab starts from the top of the page; the quieter one happens whether or not anybody is looking, because a roving tabindex puts `tabIndex={0}` on exactly one control and if that control is now in the menu then nothing in the row has it and Tab skips the whole toolbar. So the stop moves on every measurement and focus is only *called* when focus was already inside, since a window being dragged narrower is not a reason to take focus away from whatever somebody is doing elsewhere on the page. The keyboard is two conventions meeting at the trigger. The row is a toolbar: one tab stop, Left and Right between controls and swapped on an RTL page, Home and End to the ends, clamping — which is the practices' description of a toolbar and the opposite of the menu underneath, where the arrows wrap. The menu is a menu: Up and Down with wrapping, Escape back to the trigger, and choosing an item returns focus there too. They meet at the trigger, the last stop in the row and the thing Down opens, so Right, Right, Down walks out of the row and into the menu and Escape, Left walks back. A horizontal arrow pressed inside the open menu is swallowed rather than left to bubble: letting it through moves the row's tab stop underneath a menu somebody is still reading, and Escape then returns focus to a control they never left. What you get: `ToolbarOverflow`, with `label` required because a `role="toolbar"` needs an accessible name and because the trigger's name is built from it — "2 more Formatting actions", so two toolbars on a page are not both "More"; `items`, most important first, because **the order is the priority** and the row keeps a prefix while the tail folds, which is what keeps the menu in the same order as the row somebody last saw (there is deliberately no per-item priority flag: it would make the visible set an arbitrary subset, turn the arithmetic into a knapsack, and leave an item nowhere near where it was); each item `{ label, onSelect, icon?, disabled?, hideLabel?, id? }`, where `disabled` becomes `aria-disabled` and never the `disabled` attribute, so an unavailable control keeps its place in the arrow walk instead of making the walk go 1, 2, 4, and `hideLabel` shows the icon alone in the row with the label as both the accessible name and the native tooltip while the menu always shows the words. `align` picks which edge the menu lines up with and mirrors itself on an RTL page. `onVisibleCountChange` reports how many controls are in the row after each measurement, and `onOpenChange` is reported from an effect rather than from inside a state setter, so a caller is told once per change instead of twice under strict mode. `fitToolbarItems` is the whole decision as a plain function of widths, available width, gap and trigger width — the part worth asserting, since every interesting case here is a width rather than a click — and `nextToolbarStop` and `rescueToolbarStop` are the row's clamping walk and the tab-stop rescue, each on its own. Per-part `className` for the row, the controls, the trigger, the panel and its items. Nothing is portalled and nothing measures the viewport: the menu is absolutely positioned inside the row, which is what a short menu under a button needs. One file, one lucide icon, and no `radix-ui`. Distinct from the neighbours: `scroll-shadow` says there is more in a scrollable row rather than folding it, and `middle-truncate` shortens one string to fit rather than deciding which whole controls survive — it is the other component here that measures text against a box, and the probe below is the same idea at the scale of a row. `split-button` is one action with its variants under an arrow beside it, and this composes with it: the overflow menu's wrap rule is `split-button`'s own `nextMenuIndex`, imported rather than written twice, so the two menus in this registry cannot drift apart. `bulk-action-bar` is a fixed set of actions over a selection, `keyboard-shortcuts` is the sheet that lists what the keys do, `command-palette` is a searchable dialog over everything reached by a shortcut, and `filter-chips`, `segmented-control` and `multi-select` all pick values rather than running actions. `column-resizer` and `sticky-table-header` measure a table; this measures itself.

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