Skip to main content

Design system components

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

Tabs props
PropTypeDefaultDescriptionRequired
activeTabstringnoneActive tab key (controlled mode); "" is treated as absent.No
defaultActiveTabstringnoneInitially active tab key (uncontrolled mode).No
size"sm" | "md" | "lg"mdSize. Defaults to md.No
tabsTabItem[]noneArray of tab items with key, label, optional disabled/icon/count.Yes
onTabChange(key: string) => voidnoneCalled with the clicked tab's key.No
classNamestring""Optional utility classes on the tablist.No
ariaLabelstringNavigate between tabsAccessible label for the tablist. Defaults to the translated "Navigate between tabs" (also used when an empty string is passed).No
variant"pills" | "underline" | "default"defaultVisual style variant. Defaults to default.No
idPrefixstringnonePrefix 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 (default default).
  • Available sizes: sm, md, lg (default md).

Invalid combinations

  • MUST NOT: activeTab and defaultActiveTab set together. activeTab makes the component controlled and defaultActiveTab uncontrolled; pass one, not both.

Keyboard

Tabs keyboard behaviour
KeyBehaviour
ArrowLeftMoves focus or the value in that direction. (standard)
ArrowRightMoves focus or the value in that direction. (standard)
HomeMoves to the first item. (standard)
EndMoves to the last item. (standard)
ArrowUpMoves focus or the value in that direction. (standard)
ArrowDownMoves focus or the value in that direction. (standard)
EnterActivates the focused control. (standard)
SpaceActivates 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.

Tabs rules
LevelRuleHow it is checked
MUST NOTactiveTab and defaultActiveTab set together. activeTab makes the component controlled and defaultActiveTab uncontrolled; pass one, not both.Checked by the MCP tool validate_component_usage.
MUSTARIA: Tablist.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Tab.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Aria-selected.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Aria-controls.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: 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.
MUSTARIA: 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.
MUSTARIA: 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.
MUSTRequired stories produce no axe violations at the configured severity.Automated by npm run test:stories:matrix:ci. WCAG 2.5.8.
MUSTThe 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.
MUSTThe component remains legible and operable under a real browser forced-colors: active pass.Automated by npm run test:stories:hc.
MUSTEvery documented keyboard interaction works as specified.Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2.
MUSTA human completed an end-to-end accessibility review of this component.Manual review, last on 2026-11-04.
SHOULDRender one tabpanel per tab and wire it with getTabPanelProps so aria-controls/aria-labelledby stay consistent.Not stated in the contract.
SHOULDUse controlled mode (activeTab + onTabChange) when the active tab should sync with URL or app state.Not stated in the contract.
SHOULDKeep tab labels to one or two words; tabs are a switch, not a table of contents.Not stated in the contract.
SHOULDDisable a tab with disabled on its item instead of removing it, so the set stays stable while unavailable.Not stated in the contract.
SHOULD NOTDo 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 NOTMaintainers: 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/navigation exports it too.
  • Styles: import @digitaltableteur/react/style.css once at the app boundary.
  • Also available as the custom element <dt-tabs>; see the custom elements manifest.