Tab Bar with Overflow Menu

A horizontal tab bar that always keeps the active tab visible — tabs that don't fit collapse into a More menu, and an overflow tab swaps into the visible row when activated. Inspired by Vercel's project dashboard.

Navigation & SidebarsInspired by Vercel
Live PreviewInteractive
Project · Web Platform
A
acme-corp/web-platform
Overview content

Click any tab to activate it · Tabs that don't fit collapse into · Active overflow tab always slots into the visible row

TabBarOverflow.tsx
// Dependencies: react ^18, lucide-react
import React, { useState } from 'react';
import { MoreHorizontal } from 'lucide-react';

function cn(...classes: (string | false | null | undefined)[]) {
  return classes.filter(Boolean).join(' ');
}

const ALL_TABS = [
  'Overview',
  'Deployments',
  'Analytics',
  'Logs',
  'Speed Insights',
  'Firewall',
  'Storage',
  'Domains',
  'Settings',
];

const MAX_VISIBLE = 5;

export function TabBarOverflow() {
  const [activeIdx, setActiveIdx] = useState(0);
  const [overflowOpen, setOverflowOpen] = useState(false);

  const { visibleIndices, overflowIndices } = computeLayout(activeIdx);

  return (
    <div className="w-full max-w-[640px] bg-zinc-900 border border-white/[0.08] rounded-xl overflow-visible">
      <div className="flex items-center gap-2 px-5 pt-4 pb-2 border-b border-white/[0.06]">
        <div className="w-5 h-5 rounded bg-indigo-500 flex items-center justify-center shrink-0">
          <span className="text-[10px] font-bold text-white">A</span>
        </div>
        <span className="text-sm font-semibold text-white">acme-corp</span>
        <span className="text-zinc-600">/</span>
        <span className="text-sm font-semibold text-zinc-300">web-platform</span>
      </div>

      <div className="relative flex items-end gap-0 px-3 pt-2">
        {visibleIndices.map((idx) => (
          <button
            key={idx}
            onClick={() => setActiveIdx(idx)}
            className={cn(
              'relative px-3 pt-1.5 pb-3 text-sm font-medium transition-colors',
              activeIdx === idx
                ? 'text-white'
                : 'text-zinc-500 hover:text-zinc-200'
            )}
          >
            {ALL_TABS[idx]}
            {activeIdx === idx && (
              <span className="absolute bottom-0 left-1 right-1 h-[2px] rounded-full bg-indigo-500" />
            )}
          </button>
        ))}

        {overflowIndices.length > 0 && (
          <div className="relative ml-auto pb-2">
            <button
              onClick={() => setOverflowOpen((v) => !v)}
              onBlur={() => setTimeout(() => setOverflowOpen(false), 100)}
              className="flex items-center gap-1 px-2 py-1 rounded-md text-zinc-500 hover:text-zinc-200 hover:bg-white/[0.05] transition-colors"
              aria-label="More tabs"
            >
              <MoreHorizontal className="w-4 h-4" />
            </button>
            {overflowOpen && (
              <div className="absolute right-0 top-full mt-1 z-50 min-w-[180px] bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg shadow-2xl py-1">
                {overflowIndices.map((idx) => (
                  <button
                    key={idx}
                    onMouseDown={(e) => {
                      e.preventDefault();
                      setActiveIdx(idx);
                      setOverflowOpen(false);
                    }}
                    className="w-full text-left px-3 py-1.5 text-xs text-zinc-300 hover:bg-white/[0.06] hover:text-white transition-colors"
                  >
                    {ALL_TABS[idx]}
                  </button>
                ))}
              </div>
            )}
          </div>
        )}
      </div>

      <div className="h-px bg-white/[0.06]" />

      <div className="px-5 py-6 min-h-[140px] flex items-center gap-3">
        <div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse shrink-0" />
        <span className="text-sm text-zinc-400">{ALL_TABS[activeIdx]} content</span>
      </div>
    </div>
  );
}

function computeLayout(activeIdx: number) {
  const allIndices = ALL_TABS.map((_, i) => i);
  if (activeIdx < MAX_VISIBLE) {
    return {
      visibleIndices: allIndices.slice(0, MAX_VISIBLE),
      overflowIndices: allIndices.slice(MAX_VISIBLE),
    };
  }
  const head = allIndices.slice(0, MAX_VISIBLE - 1);
  return {
    visibleIndices: [...head, activeIdx],
    overflowIndices: allIndices
      .slice(MAX_VISIBLE - 1)
      .filter((i) => i !== activeIdx),
  };
}