Source Code

<div class="demo">
  <p class="hint">Resize the preview narrower — tabs that no longer fit collapse into "More"</p>
  <nav class="tabnav" id="tabnav">
    <div class="tabs" id="tabs" role="tablist">
      <button class="tab active" role="tab" aria-selected="true">Overview</button>
      <button class="tab" role="tab" aria-selected="false">Analytics</button>
      <button class="tab" role="tab" aria-selected="false">Reports</button>
      <button class="tab" role="tab" aria-selected="false">Team members</button>
      <button class="tab" role="tab" aria-selected="false">Integrations</button>
      <button class="tab" role="tab" aria-selected="false">Billing history</button>
      <button class="tab" role="tab" aria-selected="false">API keys</button>
      <button class="tab" role="tab" aria-selected="false">Audit log</button>
      <button class="tab" role="tab" aria-selected="false">Settings</button>
    </div>
    <div class="more-wrap">
      <button class="more-btn" id="moreBtn" aria-haspopup="true" aria-expanded="false" hidden>
        More <span class="more-count" id="moreCount"></span>
        <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"><path d="m6 9 6 6 6-6"/></svg>
      </button>
      <div class="more-menu" id="moreMenu" role="menu"></div>
    </div>
  </nav>
</div>

Overflow Nav Tabs — Auto-Collapse Extra Tabs into a "More" Dropdown

Nav Tabs — Overflow Collapse to "More" Menu · Navigation · Plain HTML, CSS & JS · Live preview

What's included

Features

ResizeObserver-driven measurement reacts to the container's actual box size, not just the viewport
Two-pass overflow calculation correctly reserves space for the More button before deciding what overflows
Overflowed tabs are mirrored into a real dropdown menu, not simply hidden or truncated
Clicking a tab in either the visible row or the More menu keeps active state fully in sync
role="tablist"/"tab" and role="menu"/"menuitem" semantics for assistive technology
A live badge on the More button shows exactly how many tabs are currently overflowed
Click-outside handling closes the dropdown automatically
No fixed breakpoints or hardcoded tab counts — works with any number of tabs of any label length

About this UI Snippet

Overflow-Collapsing Tab Bar — Measured, Not Media-Queried

Screenshot of the Nav Tabs — Overflow Collapse to "More" Menu snippet rendered live

A row of navigation tabs looks fine with five items on a wide screen, but add a few more tabs, or shrink the container, and they either wrap awkwardly onto a second line or get clipped off-screen. This snippet solves that with runtime width measurement rather than fixed CSS breakpoints: it actually measures how many tabs fit in the available space and moves the rest into a "More ▾" dropdown, recalculating live as the container resizes.

Why ResizeObserver instead of a media query

A media query only knows the viewport width, but this component needs to know the *container's* width, which can change independently of the viewport — a tab bar next to a resizable sidebar, inside a modal, or inside a CSS Grid area that reflows for unrelated reasons. new ResizeObserver(rebuildOverflow).observe(tabnav) calls rebuildOverflow() every time the tab bar's own box actually changes size, for any reason, which a CSS breakpoint keyed to window.innerWidth cannot reliably do.

The two-pass width calculation

rebuildOverflow() first resets every tab to visible so it can measure natural widths via offsetWidth, then walks the list accumulating a running used total against the container's clientWidth. The first pass ignores the "More" button's own width; if *any* tabs would overflow, a second pass re-runs the same accumulation but reserves 96px of space up front for the "More" button itself — otherwise the button could appear at the same moment it pushes one additional, previously-fitting tab into overflow, undercounting by one.

Moving tabs into the dropdown, not hiding them entirely

Each tab that doesn't fit gets .overflow-hidden (a plain display: none) on its original button, and a matching role="menuitem" button is generated inside #moreMenu with the same label. Clicking either the visible tab or its "More" menu counterpart calls the same setActive() function, so the active-tab state stays correctly synced regardless of which copy the user clicked, and the overflow set is recalculated immediately afterward in case the newly active tab's label width changes what fits.

Where this pattern matters most

Overflow tab collapsing is common in dashboard product settings pages, project navigation bars, and any UI where the number of tabs is data-driven (e.g. one tab per integration a user has connected) rather than fixed at design time — exactly the cases where a hardcoded breakpoint can't anticipate how many tabs will actually need to fit.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to explain why a ResizeObserver on the container is more robust here than a window resize listener or CSS media queries, and to walk through why the overflow calculation needs a second pass once the More button itself takes up space. It's also worth asking for a version that persists the currently active tab across page reloads via localStorage, or one that supports reordering tabs by drag-and-drop while keeping the overflow logic correct.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a horizontal tab navigation bar in HTML, CSS and vanilla JavaScript that automatically collapses whichever tabs don't fit into a "More" dropdown menu, recalculating live as the container is resized — no external libraries.

Requirements:
- A row of tab buttons inside a flex container that truncates (not wraps) when tabs don't fit, using role="tablist" and role="tab" for accessibility.
- Use a ResizeObserver on the tab bar's own container (not a window resize listener or media query) to detect size changes and re-run the overflow calculation whenever it fires.
- The overflow calculation must measure each tab's real rendered width and determine which tabs fit within the available space, reserving enough room for the "More" button itself so the button's own appearance doesn't cause an extra tab to be miscounted.
- Tabs that don't fit must be hidden from the main row and mirrored as items inside a dropdown menu triggered by the "More" button, using role="menu" and role="menuitem".
- Clicking a tab in either the main row or the dropdown must mark that tab active and keep the active state synchronized between both representations of it.
- The dropdown must close when clicking outside of it, and the "More" button should show a small badge with the current count of overflowed tabs.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Add or remove .tab buttons freelyEach button inside #tabs role="tablist" is picked up automatically — no manual list of labels to maintain elsewhere.
  2. 2
    Resize the container to see it reactDrag the dashed demo container's resize handle (bottom-right) narrower to watch tabs move into the More menu live.
  3. 3
    Adjust the reserved space constantThe reserve variable (96px) in rebuildOverflow approximates the More button's width — increase it if your button styling is wider.
  4. 4
    Style the active stateBoth .tab.active and .more-menu button.active are kept in sync by setActive() — restyle either selector to change the active look.
  5. 5
    Hook up real navigationReplace the plain setActive(label) calls with your router's navigation call, keeping the same active-tab bookkeeping.

Real-world uses

Common Use Cases

SAAS
Product Settings Navigation
Handle a data-driven number of settings tabs (one per connected integration, etc.) without breaking layout.
Dashboard Section Tabs
Keep a dashboard's top-level section tabs usable inside variable-width panels or resizable layouts.
RESPONSIVE
Responsive Without Media Queries
A cleaner alternative to writing multiple max-width breakpoints for a tab count that can change per user or account.
ADMIN
Admin Panel Record Views
Tab bars on a record detail page (Overview, Activity, Billing, etc.) that must never wrap or clip regardless of screen size.
Related: Pagination
See the Pagination for a related navigation pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

A horizontally scrolling tab bar hides how many more tabs exist and requires horizontal scrolling to discover them; collapsing overflow into a labeled "More" dropdown keeps every tab discoverable and countable at a glance.

The first pass measures which tabs fit assuming no More button is needed. If any tabs overflow, the More button itself will take up space, so a second pass re-measures with that space reserved — without this, the last "fitting" tab could visually collide with the newly appearing button.

Yes — the measurement is based on each tab's actual rendered offsetWidth, not a hardcoded character count, so it correctly adapts to longer translated strings or dynamically inserted tab labels.

The ResizeObserver is attached to the tab bar container itself, so any change to its box size — a sidebar toggling, a parent flex/grid reflow, a font loading and changing text width — triggers rebuildOverflow(), not just a browser window resize.

Both the visible .tab buttons and the generated .more-menu items call the same setActive(label) function on click, which updates aria-selected and the .active class on every matching element by label text, whichever version was clicked.

Yes — as long as each tab button's total rendered width (text plus icon) is accurately reflected in offsetWidth, which it will be for normal inline content; just make sure setActive's text-matching logic accounts for icon markup if you add icons inside the button.