Tabs
The Tabs component lets users switch between related views without leaving the page. It supports two visual variants, three sizes, controlled and uncontrolled modes, and disabled tabs — all built on Headless UI's TabGroup with full keyboard navigation.
Installation
npm install @libretexts/davis-react
Basic Usage
import { Tabs } from '@libretexts/davis-react';
export default function Example() {
return (
<Tabs>
<Tabs.List>
<Tabs.Tab>Overview</Tabs.Tab>
<Tabs.Tab>Features</Tabs.Tab>
<Tabs.Tab>Pricing</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel>Overview content</Tabs.Panel>
<Tabs.Panel>Features content</Tabs.Panel>
<Tabs.Panel>Pricing content</Tabs.Panel>
</Tabs.Panels>
</Tabs>
);
}
Variants
Two variants control how the active tab is indicated.
{/* Line: underline indicator on the active tab (default) */}
<Tabs variant="line">
<Tabs.List>
<Tabs.Tab>Overview</Tabs.Tab>
<Tabs.Tab>Features</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel>Overview content</Tabs.Panel>
<Tabs.Panel>Features content</Tabs.Panel>
</Tabs.Panels>
</Tabs>
{/* Pills: pill-shaped background on the active tab */}
<Tabs variant="pills">
<Tabs.List>
<Tabs.Tab>Overview</Tabs.Tab>
<Tabs.Tab>Features</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel>Overview content</Tabs.Panel>
<Tabs.Panel>Features content</Tabs.Panel>
</Tabs.Panels>
</Tabs>
Pills Color
When using the pills variant, the color prop controls the active tab's color scheme.
<Tabs variant="pills" color="white">
<Tabs.List>
<Tabs.Tab>White pill (default)</Tabs.Tab>
<Tabs.Tab>Another tab</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel>White pill content</Tabs.Panel>
<Tabs.Panel>Another panel</Tabs.Panel>
</Tabs.Panels>
</Tabs>
<Tabs variant="pills" color="primary">
<Tabs.List>
<Tabs.Tab>Primary pill</Tabs.Tab>
<Tabs.Tab>Another tab</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel>Primary pill content</Tabs.Panel>
<Tabs.Panel>Another panel</Tabs.Panel>
</Tabs.Panels>
</Tabs>
The color prop has no effect when variant="line".
Sizes
<Tabs size="sm">
<Tabs.List>
<Tabs.Tab>Small</Tabs.Tab>
<Tabs.Tab>Tabs</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel>Small content</Tabs.Panel>
<Tabs.Panel>More content</Tabs.Panel>
</Tabs.Panels>
</Tabs>
<Tabs size="md">...</Tabs>
<Tabs size="lg">...</Tabs>
Disabled Tabs
Add the disabled prop to individual Tabs.Tab elements to prevent selection.
<Tabs variant="line" size="md">
<Tabs.List>
<Tabs.Tab>Overview</Tabs.Tab>
<Tabs.Tab>Features</Tabs.Tab>
<Tabs.Tab disabled>Disabled</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel>Overview content</Tabs.Panel>
<Tabs.Panel>Features content</Tabs.Panel>
<Tabs.Panel>Disabled content</Tabs.Panel>
</Tabs.Panels>
</Tabs>
Controlled Usage
Control the selected tab externally using selectedIndex and onChange.
import { Tabs } from '@libretexts/davis-react';
import { useState } from 'react';
export default function ControlledTabs() {
const [selected, setSelected] = useState(0);
return (
<>
<p>Active tab index: {selected}</p>
<Tabs selectedIndex={selected} onChange={setSelected}>
<Tabs.List>
<Tabs.Tab>Overview</Tabs.Tab>
<Tabs.Tab>Features</Tabs.Tab>
<Tabs.Tab>Pricing</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel>Overview content</Tabs.Panel>
<Tabs.Panel>Features content</Tabs.Panel>
<Tabs.Panel>Pricing content</Tabs.Panel>
</Tabs.Panels>
</Tabs>
</>
);
}
Use defaultIndex instead of selectedIndex if you only need to set the initial tab without managing state.
Props
Tabs
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'line' | 'pills' | 'line' | Visual style of the tab indicators |
size | 'sm' | 'md' | 'lg' | 'md' | Controls tab padding and font size |
color | 'white' | 'primary' | 'white' | Active pill color (pills variant only) |
defaultIndex | number | 0 | Initially selected tab (uncontrolled) |
selectedIndex | number | - | Currently selected tab (controlled) |
onChange | (index: number) => void | - | Called when the selected tab changes |
className | string | - | Additional CSS classes |
Tabs.Tab
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Prevents the tab from being selected |
className | string | - | Additional CSS classes |
children | ReactNode | - | Tab label content |
Tabs.List, Tabs.Panels, Tabs.Panel
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | - | Child content |
Accessibility
The Tabs component is built on Headless UI's TabGroup and implements the ARIA Tabs pattern:
Tabs.Listrenders withrole="tablist"- Each
Tabs.Tabrenders withrole="tab",aria-selected, andaria-controlspointing to its panel - Each
Tabs.Panelrenders withrole="tabpanel"andaria-labelledbypointing to its tab - Left/Right arrow keys move focus between tabs
- Home moves focus to the first tab; End moves to the last
- Space or Enter selects the focused tab
- Disabled tabs are skipped during keyboard navigation and receive
aria-disabled="true"
Best practices:
- Keep tab labels short — one or two words — so the tab list is easy to scan
- Each tab panel should contain meaningfully distinct content; avoid using tabs for sequential steps (use a stepper instead)
- Do not use tabs inside tabs; nested tab groups create confusing navigation hierarchies