Tabs
Tablist navigation with keyboard support, a sliding selection indicator, and per-item icon/count.
Status: stable. Group: navigation. Markdown version: /design-system/components/tabs.md
When to use
Tabs renders the tablist only; the parent owns the panels, pairing each to its tab with the exported getTabPanelProps(key, isActive) helper (or matching id/aria-labelledby/hidden attributes by hand). Drive it controlled with activeTab + onTabChange, or uncontrolled with defaultActiveTab. Arrow keys and Home/End move focus between tabs (wrapping at the ends) and Enter or Space selects the focused tab; arrows and Home/End skip disabled tabs; variant picks the visual language (default, pills, underline) and size the density. Tabs switch views in place; they are not page navigation.
When not to use
- Do not nest tablists inside another tablist. APG explicitly disallows it; keyboard navigation becomes ambiguous.
- Do not use Tabs for non-mutually-exclusive content. Two panels open at once is a job for Accordion (in a future
mode="multi") or side-by-side layout. - Do not use tabs for page navigation; that is NavLink in a nav landmark; tabs switch views inside one page.
Look-alikes
- Accordion: Do not use Tabs for non-mutually-exclusive content. Two panels open at once is a job for Accordion (in a future
mode="multi") or side-by-side layout. - FilterChip: Do not use FilterChip as navigation between content panels; use Tabs.
- NavLink: Do not use tabs for page navigation; that is NavLink in a nav landmark; tabs switch views inside one page.
Props
| Prop | Type | Default | Description | Required |
|---|---|---|---|---|
activeTab | string | none | Active tab key (controlled mode); "" is treated as absent. | No |
defaultActiveTab | string | none | Initially active tab key (uncontrolled mode). | No |
size | "sm" | "md" | "lg" | md | Size. Defaults to md. | No |
tabs | TabItem[] | none | Array of tab items with key, label, optional disabled/icon/count. | Yes |
onTabChange | (key: string) => void | none | Called with the clicked tab's key. | No |
className | string | "" | Optional utility classes on the tablist. | No |
ariaLabel | string | Navigate between tabs | Accessible label for the tablist. Defaults to the translated "Navigate between tabs" (also used when an empty string is passed). | No |
variant | "pills" | "underline" | "default" | default | Visual style variant. Defaults to default. | No |
idPrefix | string | none | Prefix for the tab and panel ids (prefix-tab-key, prefix-tabpanel-key) so two Tabs on one page can share keys. Pass the same value to getTabPanelProps. Omit it for the unprefixed tab-key ids. | No |
Variants
- Available variants:
default,pills,underline(defaultdefault). - Available sizes:
sm,md,lg(defaultmd).
Invalid combinations
- MUST NOT:
activeTabanddefaultActiveTabset together.activeTabmakes the component controlled anddefaultActiveTabuncontrolled; pass one, not both.
Keyboard
| Key | Behaviour |
|---|---|
| ArrowLeft | Moves focus or the value in that direction. (standard) |
| ArrowRight | Moves focus or the value in that direction. (standard) |
| Home | Moves to the first item. (standard) |
| End | Moves to the last item. (standard) |
| ArrowUp | Moves focus or the value in that direction. (standard) |
| ArrowDown | Moves focus or the value in that direction. (standard) |
| Enter | Activates the focused control. (standard) |
| Space | Activates or toggles the focused control. (standard) |
Rows marked (standard) name a key the contract lists without a description; the behaviour shown is the usual one for that key.
Rules
Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.
| Level | Rule | How it is checked |
|---|---|---|
| MUST NOT | activeTab and defaultActiveTab set together. activeTab makes the component controlled and defaultActiveTab uncontrolled; pass one, not both. | Checked by the MCP tool validate_component_usage. |
| MUST | ARIA: Tablist. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Tab. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Aria-selected. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Aria-controls. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Manual activation: arrows and Home/End move focus (wrapping), Enter or Space selects the focused tab; only the active tab is in the Tab order. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Arrows, Home and End skip disabled tabs, so every enabled tab is reachable with the keyboard. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Tab and panel ids are tab-<key> and tabpanel-<key>, or <idPrefix>-tab-<key> and <idPrefix>-tabpanel-<key> when idPrefix is set; two Tabs on one page that share keys need distinct idPrefix values (a development warning names a duplicate id) | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | Required stories produce no axe violations at the configured severity. | Automated by npm run test:stories:matrix:ci. WCAG 2.5.8. |
| MUST | The accessibility tree (role, name, state) matches the committed snapshot for every required story. | Automated by npm run test:stories:matrix:ci. WCAG 1.3.1, 4.1.2. |
| MUST | The component remains legible and operable under a real browser forced-colors: active pass. | Automated by npm run test:stories:hc. |
| MUST | Every documented keyboard interaction works as specified. | Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2. |
| MUST | A human completed an end-to-end accessibility review of this component. | Manual review, last on 2026-11-04. |
| SHOULD | Render one tabpanel per tab and wire it with getTabPanelProps so aria-controls/aria-labelledby stay consistent. | Not stated in the contract. |
| SHOULD | Use controlled mode (activeTab + onTabChange) when the active tab should sync with URL or app state. | Not stated in the contract. |
| SHOULD | Keep tab labels to one or two words; tabs are a switch, not a table of contents. | Not stated in the contract. |
| SHOULD | Disable a tab with disabled on its item instead of removing it, so the set stays stable while unavailable. | Not stated in the contract. |
| SHOULD NOT | Do not lazy-delete panel DOM for inactive tabs that hold form state; hide them (hidden attr) so user input survives switching. | Not stated in the contract. |
| MUST NOT | Maintainers: Do not hand-author id="tab-<key>" / id="tabpanel-<key>" on the panel side. Use getTabPanelProps; the id convention is internal. When two Tabs on one page share tab keys, give each a distinct idPrefix and pass it to getTabPanelProps(key, isActive, idPrefix). | Release review; not machine-checked. |
Install and import
- Install:
npm install @digitaltableteur/react(version 0.9.1, restricted access on npm). - Import:
import { Tabs } from "@digitaltableteur/react"; - Entry point:
@digitaltableteur/react/navigationexports it too. - Styles: import
@digitaltableteur/react/style.cssonce at the app boundary. - Also available as the custom element
<dt-tabs>; see the custom elements manifest.
Links
- Machine-readable contract (Design System Contract 1.0)
- Markdown version of this page
- Component index as JSON
- All components
- Source: The implementation lives in a private repository. This page, its Markdown twin and the contract are generated from it at build time and are the public source of truth.