Skip to main content

Tabs

  • Component overview: Tabs organize content into panels and switch between them. The public API is declarative: children are treated as triggers in simple cases, and panel content is configured through items.
  • Size baseline: Container height=40px, tab gap=24px (Default/Underline) or 16px (Chevron).
  • Implementation note: ui/tabs retains only the structural skeleton; the design layer owns all visual tokens. The variant prop is configured directly on Tabs.
  • Figma spec

Basic Usage​

Result
Loading...
Live Editor
<Tabs
  defaultValue="tab1"
  items={[
    {
      value: 'tab1',
      label: 'Account',
      content: (
        <p style={{ padding: '16px 0', color: '#333' }}>
          Manage your account settings and preferences.
        </p>
      ),
    },
    {
      value: 'tab2',
      label: 'Settings',
      content: (
        <p style={{ padding: '16px 0', color: '#333' }}>Customize your application settings.</p>
      ),
    },
    {
      value: 'tab3',
      label: 'Notifications',
      content: (
        <p style={{ padding: '16px 0', color: '#333' }}>Configure your notification preferences.</p>
      ),
    },
  ]}
/>

Variants​

3 visual variants to fit different contexts.

Default​

Plain text tabs, minimal visual style.

Result
Loading...
Live Editor
<Tabs
  defaultValue="tab1"
  variant="default"
  items={[
    { value: 'tab1', label: 'Tab 1', content: <p style={{ padding: '16px 0' }}>Content 1</p> },
    { value: 'tab2', label: 'Tab 2', content: <p style={{ padding: '16px 0' }}>Content 2</p> },
    { value: 'tab3', label: 'Tab 3', content: <p style={{ padding: '16px 0' }}>Content 3</p> },
  ]}
/>

Underline​

Active tab has a 2px bottom border indicator.

Result
Loading...
Live Editor
<Tabs
  defaultValue="tab1"
  variant="underline"
  items={[
    { value: 'tab1', label: 'Tab 1', content: <p style={{ padding: '16px 0' }}>Content 1</p> },
    { value: 'tab2', label: 'Tab 2', content: <p style={{ padding: '16px 0' }}>Content 2</p> },
    { value: 'tab3', label: 'Tab 3', content: <p style={{ padding: '16px 0' }}>Content 3</p> },
  ]}
/>

Chevron​

Active tab shows a chevron down icon, hinting at expandable content.

Result
Loading...
Live Editor
<Tabs
  defaultValue="tab1"
  variant="chevron"
  items={[
    { value: 'tab1', label: 'Tab 1', content: <p style={{ padding: '16px 0' }}>Content 1</p> },
    { value: 'tab2', label: 'Tab 2', content: <p style={{ padding: '16px 0' }}>Content 2</p> },
    { value: 'tab3', label: 'Tab 3', content: <p style={{ padding: '16px 0' }}>Content 3</p> },
  ]}
/>

Variant Comparison​

All three variants side by side.

Result
Loading...
Live Editor
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
  <div>
    <p style={{ fontSize: 12, color: '#999', marginBottom: 8 }}>Default</p>
    <Tabs
      defaultValue="tab1"
      variant="default"
      items={[
        { value: 'tab1', label: 'Tab 1' },
        { value: 'tab2', label: 'Tab 2' },
      ]}
    />
  </div>
  <div>
    <p style={{ fontSize: 12, color: '#999', marginBottom: 8 }}>Underline</p>
    <Tabs
      defaultValue="tab1"
      variant="underline"
      items={[
        { value: 'tab1', label: 'Tab 1' },
        { value: 'tab2', label: 'Tab 2' },
      ]}
    />
  </div>
  <div>
    <p style={{ fontSize: 12, color: '#999', marginBottom: 8 }}>Chevron</p>
    <Tabs
      defaultValue="tab1"
      variant="chevron"
      items={[
        { value: 'tab1', label: 'Tab 1' },
        { value: 'tab2', label: 'Tab 2' },
      ]}
    />
  </div>
</div>

Disabled State​

Individual tabs can be disabled.

Result
Loading...
Live Editor
<Tabs
  defaultValue="tab1"
  variant="underline"
  items={[
    {
      value: 'tab1',
      label: 'Active',
      content: <p style={{ padding: '16px 0' }}>Active content</p>,
    },
    {
      value: 'tab2',
      label: 'Disabled',
      disabled: true,
      content: <p style={{ padding: '16px 0' }}>Disabled content</p>,
    },
    {
      value: 'tab3',
      label: 'Normal',
      content: <p style={{ padding: '16px 0' }}>Normal content</p>,
    },
  ]}
/>

Scrollable Overflow​

When there are many tabs, the list automatically becomes horizontally scrollable with left/right navigation arrows.

Result
Loading...
Live Editor
<Tabs
  defaultValue="t1"
  variant="underline"
  items={[
    {
      value: 't1',
      label: 'Dashboard',
      content: <p style={{ padding: '16px 0' }}>Dashboard content</p>,
    },
    { value: 't2', label: 'Analytics' },
    { value: 't3', label: 'Reports' },
    { value: 't4', label: 'Notifications' },
    { value: 't5', label: 'Settings' },
    { value: 't6', label: 'Integrations' },
    { value: 't7', label: 'Billing' },
    { value: 't8', label: 'Security' },
    { value: 't9', label: 'Team' },
    { value: 't10', label: 'API Keys' },
  ]}
/>

Preserve State (destroyOnHidden)​

By default, inactive tab panels are unmounted. Set destroyOnHidden={false} to keep them in the DOM, preserving internal state like scroll position or form input.

Result
Loading...
Live Editor
const PreserveStateExample = () => {
  const [count, setCount] = useState(0)
  return (
    <Tabs
      defaultValue="tab1"
      items={[
        {
          value: 'tab1',
          label: 'Counter',
          destroyOnHidden: false,
          content: (
            <div style={{ padding: '16px 0' }}>
              <p>Count: {count}</p>
              <button
                onClick={() => setCount((current) => current + 1)}
                style={{
                  marginTop: 8,
                  padding: '4px 12px',
                  border: '1px solid #ccc',
                  borderRadius: 4,
                }}
              >
                Increment
              </button>
              <p style={{ fontSize: 12, color: '#999', marginTop: 8 }}>
                Switch tabs — the count is preserved.
              </p>
            </div>
          ),
        },
        {
          value: 'tab2',
          label: 'Other',
          destroyOnHidden: false,
          content: (
            <p style={{ padding: '16px 0' }}>
              Other content. Go back to see the count is still there.
            </p>
          ),
        },
      ]}
    />
  )
}

render(<PreserveStateExample />)

Size Spec​

DimensionValue
Container height40px
Tab gap (Default)24px
Tab gap (Underline)24px
Tab gap (Chevron)16px
Font size14px
Line height20px
Active font weight600 (semibold)
Inactive font weight400 (normal)
Underline thickness2px
Chevron icon size20×20px

Props​

Tabs​

PropTypeDefaultDescription
itemsTabsItem[]-Declarative tab items
variant'default' | 'underline' | 'chevron''default'Visual variant
defaultValuestring-Default active tab (uncontrolled)
valuestring-Active tab (controlled)
onValueChange(value: string) => void-Callback when active tab changes
classNamestring-Custom wrapper class name
listClassNamestring-Custom class for the tab list

TabsItem​

FieldTypeDefaultDescription
valuestring-Unique value identifying the tab
labelReactNode-Trigger content
contentReactNode-Panel content
disabledbooleanfalseDisables the tab trigger
triggerClassNamestring-Custom class applied to the trigger
contentClassNamestring-Custom class applied to the panel
destroyOnHiddenbooleantrueKeep inactive panel mounted when false