Tabs
Switches between related panels of content on one page. Horizontal and vertical, three sizes, icons and a divider, with arrow keys that follow the page direction.
Version 0.4.1 ·
Installation
bunx --bun shadcn@latest add @dga/tabsUsage
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export function RequestTabs() {
return (
<Tabs defaultValue="details">
<TabsList aria-label="Request sections">
<TabsTrigger value="details">Details</TabsTrigger>
<TabsTrigger value="documents">Documents</TabsTrigger>
<TabsTrigger value="history">History</TabsTrigger>
</TabsList>
<TabsContent value="details">Applicant name, service and submission date.</TabsContent>
<TabsContent value="documents">Two files attached.</TabsContent>
<TabsContent value="history">Submitted on 4 November 2026.</TabsContent>
</Tabs>
);
}Use Tabs to split one page into parts that belong together and are read one at a time: the details, documents and history of a request. Tabs do not change the page: for moving between pages use links or a navigation menu. Tabs are not steps; use a stepper for a sequence. Every TabsTrigger needs a value, and the TabsContent with the same value is its panel. Give the list an aria-label if the page has more than one.
Anatomy
- List (
role="tablist"): the row of tabs, with a 3px divider under it (divider). It scrolls sideways when the tabs do not fit. - Tab (
role="tab"): an optional icon and a label. The selected tab is bold with a 3px bar inborder-primary. - Panel (
role="tabpanel"): the content of the selected tab.
Examples
Sizes and icons
size is sm, md (default) or lg. Put an icon before the label. A disabled tab can be reached with the arrow keys but not selected.
Vertical
orientation="vertical" puts the list beside the panel, with the bar on the end side. Up and Down move between tabs.
Behaviour
By default an arrow key moves focus and selects the tab. Set activateOnFocus={false} on TabsList for manual activation: arrows only move focus, and Enter or Space selects. divider={false} removes the line under the list, flush removes its side padding, and value with onValueChange makes the selection yours to control.
RTL
The list starts on the right, and the Left and Right arrows swap: in Arabic, Left moves to the next tab. The components read the direction from <html dir> themselves, so you do not wrap them in a provider.
Accessibility
| Key | Action |
|---|---|
| Tab | Moves focus into the list (to the selected tab), then into the panel. |
| ArrowRight | Moves to the next tab and opens its panel (the previous tab in Arabic). |
| ArrowLeft | Moves to the previous tab (the next tab in Arabic). |
| ArrowDown | In a vertical list, moves to the next tab (ArrowUp to the previous). |
| Home | Moves to the first tab (and End to the last). |
- Built on Base UI Tabs:
role="tablist",tabandtabpanel, witharia-selected,aria-controlsandaria-orientation. Only the selected tab is in the tab order (roving focus). - A tab opens its panel as soon as it gets focus. The panel itself is focusable, so keyboard users can reach content that has no focusable control.
- The selected tab is shown by bold text and a bar, not colour alone. Text is 4.5:1, icons and the bar 3:1 or better, on every surface, in light and dark. Focus is a 2px outline inside the tab.
API Reference
Tabs
Switches between related panels of content on one page (DGA: Tabs). One tab is in the tab order; the arrow keys move between tabs (Left and Right swap on Arabic pages, Up and Down in a vertical list), and Home and End jump to the first and last. A tab opens its panel on focus.
| Prop | Type | Default |
|---|---|---|
"horizontal" | "vertical"The component orientation (layout flow direction). | "horizontal" | |
| ||
string | numberThe default value. Use when the component is not controlled. When the value is `null`, no Tab will be active. | No default | |
| ||
(value) => voidCallback invoked when new value is being set. The event `reason` is `'none'` for user-initiated changes, such as a click or keyboard navigation; `'initial'` for the first automatic selection or fallback in uncontrolled roots when `defaultValue` is omitted or `undefined`, including when the implicit initial value is disabled or missing; `'disabled'` for automatic fallback when the selected tab becomes disabled in uncontrolled roots; or `'missing'` for automatic fallback when the selected tab is removed, or when an explicit `defaultValue` never matches a mounted tab in uncontrolled roots. For automatic changes, the selected value can be `null` when no enabled Tab is available as a fallback. Automatic changes cannot be canceled; calling `eventDetails.cancel()` for `'initial'`, `'disabled'`, or `'missing'` has no effect. | No default | |
| ||
string | numberThe value of the currently active `Tab`. Use when the component is controlled. When the value is `null`, no Tab will be active. | No default | |
| ||
Also accepts every prop of the Base UI Tabs (opens in a new tab).
TabsContent
The content a tab shows. It is hidden while its tab is not selected.
| Prop | Type | Default |
|---|---|---|
string | numberThe value of the TabPanel. It will be shown when the Tab with the corresponding value is active. | No default | |
| ||
Also accepts every prop of the Base UI Tabs (opens in a new tab).
TabsList
The row (or column, in a vertical Tabs) of tabs. Give it an `aria-label` when there are several lists.
| Prop | Type | Default |
|---|---|---|
"sm" | "md" | "lg"Padding and text: `sm`, `md` or `lg`. | "md" | |
| ||
booleanA 3px line under (or beside) the list. Default `true`. | true | |
| ||
booleanNo inline padding at the start of the list. | false | |
| ||
booleanScrolls sideways instead of wrapping when the tabs do not fit. | true | |
| ||
booleanWhether to automatically change the active tab on arrow key focus. Otherwise, tabs will be activated using <kbd>Enter</kbd> or <kbd>Space</kbd> key press. | true | |
| ||
Also accepts every prop of the Base UI Tabs (opens in a new tab).
TabsTrigger
One tab. Put an icon first and the label after it.
| Prop | Type | Default |
|---|---|---|
booleanWhether the Tab is disabled. If a first Tab on a `<Tabs.List>` is disabled, it won't initially be selected. Instead, the next enabled Tab will be selected. However, it does not work like this during server-side rendering, as it is not known during pre-rendering which Tabs are disabled. To work around it, ensure that `defaultValue` or `value` on `<Tabs.Root>` is set to an enabled Tab's value. | No default | |
| ||
string | numberThe value of the Tab. | No default | |
| ||
Also accepts every prop of the Base UI Tabs (opens in a new tab).
Deviations from DGA
- The More tab (an overflow menu on horizontal tabs) is not built. A list that does not fit scrolls sideways instead; for an overflow menu, put a Menu in the last tab.
- Vertical tabs also show a neutral bar on hover and press (as in the DGA CSS); every tab shows the primary bar when selected.
| Change | Reason |
|---|---|
Tabs: selected, hover and More tab | A scrolling list needs no extra control and keeps every tab reachable with Tab and the arrow keys. The overflow menu is on the roadmap. |
Tabs, Content Switcher: arrow keys in RTL | The arrow keys must match what the eyes see, in both directions (WAI-ARIA tabs pattern). |
Contrast
78 pairs checked · all pass WCAG 2.1 AAShow 13 statesHide
| Variant / state | Light | Dark |
|---|---|---|
| selected text | 17.31:1 AAA | 15.87:1 AAA |
| unselected text | 9.73:1 AAA | 12.04:1 AAA |
| unselected icon | 9.73:1 3:1 | 12.04:1 3:1 |
| selected icon | 17.31:1 3:1 | 15.87:1 3:1 |
| selected bar | 4.54:1 3:1 | 7.73:1 3:1 |
| focus ring | 17.31:1 3:1 | 17.49:1 3:1 |
| panel text | 17.31:1 AAA | 15.87:1 AAA |
| text / hovered | 16.44:1 AAA | 9.59:1 AAA |
| unselected icon / hovered | 9.24:1 3:1 | 7.28:1 3:1 |
| focus ring / hovered | 16.44:1 3:1 | 10.57:1 3:1 |
| text / pressed | 14.61:1 AAA | 6.95:1 AA |
| unselected icon / pressed | 8.22:1 3:1 | 5.27:1 3:1 |
| focus ring / pressed | 14.61:1 3:1 | 7.66:1 3:1 |
Text needs 4.5:1, icons and focus rings 3:1. Disabled states are exempt. Full contrast report