Tabs
- Component overview: Tabs organize content into panels and switch between them. The public API is declarative:
childrenare treated as triggers in simple cases, and panel content is configured throughitems. - Size baseline: Container height=40px, tab gap=24px (Default/Underline) or 16px (Chevron).
- Implementation note:
ui/tabsretains only the structural skeleton; the design layer owns all visual tokens. Thevariantprop is configured directly onTabs. - 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
| Dimension | Value |
|---|---|
| Container height | 40px |
| Tab gap (Default) | 24px |
| Tab gap (Underline) | 24px |
| Tab gap (Chevron) | 16px |
| Font size | 14px |
| Line height | 22px |
| Active font weight | 500 (medium) |
| Inactive font weight | 400 (normal) |
| Underline thickness | 2px |
| Chevron icon size | 20×20px |
Props
Tabs
| Prop | Type | Default | Description |
|---|---|---|---|
items | TabsItem[] | - | Declarative tab items |
variant | 'default' | 'underline' | 'chevron' | 'default' | Visual variant |
defaultValue | string | - | Default active tab (uncontrolled) |
value | string | - | Active tab (controlled) |
onValueChange | (value: string) => void | - | Callback when active tab changes |
className | string | - | Custom wrapper class name |
listClassName | string | - | Custom class for the tab list |
TabsItem
| Field | Type | Default | Description |
|---|---|---|---|
value | string | - | Unique value identifying the tab |
label | ReactNode | - | Trigger content |
content | ReactNode | - | Panel content |
disabled | boolean | false | Disables the tab trigger |
triggerClassName | string | - | Custom class applied to the trigger |
contentClassName | string | - | Custom class applied to the panel |
destroyOnHidden | boolean | true | Keep inactive panel mounted when false |