OverflowTabs

free

OverflowTabs measures a tab strip against its available width and tucks whatever doesn't fit behind a "More" dropdown.

@floating-ui/react
Preview
Dark
import { useState } from 'react';
import OverflowTabs from '@/components/composites/OverflowTabs';

const sections = [
  { label: 'Basic Info', value: 'basic' },
  { label: 'Pricing', value: 'pricing' },
  { label: 'Inventory', value: 'inventory' },
  { label: 'Images', value: 'images' },
  { label: 'Shipping', value: 'shipping' },
  { label: 'SEO', value: 'seo' },
  { label: 'Help', value: 'help' },
];

export default function ValidationDemo() {
  const [value, setValue] = useState('basic');

  return (
    <OverflowTabs
      tabList={sections.map((section) => ({
        value: section.value,
        label: section.label,
      }))}
      value={value}
      onChange={setValue}
    />
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add OverflowTabs

Examples

With Actions

Preview
Dark
import { useState } from 'react';
import { PiPlus } from 'react-icons/pi'
import Button from '@/components/ui/Button';
import OverflowTabs from '@/components/composites/OverflowTabs';

const tabList = [
  { label: 'Details', value: 'details' },
  { label: 'Comments', value: 'comments' },
  { label: 'History', value: 'history' },
  { label: 'Attachments', value: 'attachments' },
  { label: 'Activity', value: 'activity' },
];

export default function WithActionsDemo() {
  const [value, setValue] = useState('details');

  return (
    <OverflowTabs 
      tabList={tabList} 
      value={value} 
      onChange={setValue}
      tabListClass="border-0"
      className="border-b "
    >
      <Button size="sm" icon={<PiPlus />}>
        New
      </Button>
    </OverflowTabs>
  );
}

Icon Labels

Preview
Dark
import { useState } from 'react';
import {
    PiClockCounterClockwise,
    PiCreditCard,
    PiShieldCheck,
    PiSquaresFour,
    PiUsers,
    PiWebhooksLogo,
} from 'react-icons/pi'
import OverflowTabs from '@/components/composites/OverflowTabs';

const sections = [
  { label: 'General', value: 'general', icon: <PiSquaresFour /> },
  { label: 'Members', value: 'members', icon: <PiUsers /> },
  { label: 'Roles & Access', value: 'roles', icon: <PiShieldCheck /> },
  { label: 'Billing', value: 'billing', icon: <PiCreditCard /> },
  { label: 'Webhooks', value: 'webhooks', icon: <PiWebhooksLogo /> },
  { label: 'Audit Log', value: 'audit', icon: <PiClockCounterClockwise /> },
];

export default function IconLabelsDemo() {
  const [value, setValue] = useState('general');

  return (
    <OverflowTabs
      tabList={sections.map((section) => ({
        value: section.value,
        label: (
          <span className="flex items-center gap-2">
            {section.icon}
            {section.label}
          </span>
        ),
      }))}
      value={value}
      onChange={setValue}
      tabNavClass="min-w-[100px] text-center"
    />
  );
}

API

PropDescriptionTypeDefault
tabListTabs to render. label accepts any ReactNode, so icons or inline state indicators compose directly into it — there's no separate icon field (see Icon Labels).OverflowTabsTab[]
valueCurrently active tab. When the active tab has overflowed into the dropdown, the visible tab strip shows no active indicator and the "More" button takes its place instead.string
onChangeCalled with the new tab's value, whether it's clicked in the visible strip or in the overflow dropdown.(value: string) => void
childrenExtra content docked after the tabs, such as an action button. Its width is included in the overflow calculation.ReactNode
tabListClassClass names applied to the tab list container.string
tabNavClassClass names applied to each visible tab.string
classNameClass names applied to the inner Tabs root, not the outer container.string
...restNative div props, applied to the outer container.NativeOverflowTabsDivProps

Types

type OverflowTabsTab = {
  label: string | ReactNode;
  value: string;
}
 
type NativeOverflowTabsDivProps = Omit<ComponentProps<'div'>, 'onChange'>