Skip to content

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 ·

Tabs

Installation

bunx --bun shadcn@latest add @dga/tabs

Usage

page.tsx
tsx
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 in border-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.

Sizes and icons

Vertical

orientation="vertical" puts the list beside the panel, with the bar on the end side. Up and Down move between tabs.

Vertical

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.

Behaviour

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.

LTR (English)
RTL (Arabic)

Accessibility

KeyAction
TabMoves focus into the list (to the selected tab), then into the panel.
ArrowRightMoves to the next tab and opens its panel (the previous tab in Arabic).
ArrowLeftMoves to the previous tab (the next tab in Arabic).
ArrowDownIn a vertical list, moves to the next tab (ArrowUp to the previous).
HomeMoves to the first tab (and End to the last).
  • Built on Base UI Tabs: role="tablist", tab and tabpanel, with aria-selected, aria-controls and aria-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.

Tabs props
PropTypeDefault
"horizontal" | "vertical"

The component orientation (layout flow direction).

"horizontal"
string | number

The default value. Use when the component is not controlled. When the value is `null`, no Tab will be active.

No default
(value) => void

Callback 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 | number

The 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.

TabsContent props
PropTypeDefault
string | number

The 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.

TabsList props
PropTypeDefault
"sm" | "md" | "lg"

Padding and text: `sm`, `md` or `lg`.

"md"
boolean

A 3px line under (or beside) the list. Default `true`.

true
boolean

No inline padding at the start of the list.

false
boolean

Scrolls sideways instead of wrapping when the tabs do not fit.

true
boolean

Whether 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.

TabsTrigger props
PropTypeDefault
boolean

Whether 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 | number

The 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.
ChangeReason
Tabs: selected, hover and More tabA 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 RTLThe arrow keys must match what the eyes see, in both directions (WAI-ARIA tabs pattern).

View all 2 deviations for this component

Contrast

78 pairs checked · all pass WCAG 2.1 AAShow 13 states
Variant / stateLightDark
selected text17.31:1 AAA15.87:1 AAA
unselected text9.73:1 AAA12.04:1 AAA
unselected icon9.73:1 3:112.04:1 3:1
selected icon17.31:1 3:115.87:1 3:1
selected bar4.54:1 3:17.73:1 3:1
focus ring17.31:1 3:117.49:1 3:1
panel text17.31:1 AAA15.87:1 AAA
text / hovered16.44:1 AAA9.59:1 AAA
unselected icon / hovered9.24:1 3:17.28:1 3:1
focus ring / hovered16.44:1 3:110.57:1 3:1
text / pressed14.61:1 AAA6.95:1 AA
unselected icon / pressed8.22:1 3:15.27:1 3:1
focus ring / pressed14.61:1 3:17.66:1 3:1

Text needs 4.5:1, icons and focus rings 3:1. Disabled states are exempt. Full contrast report