Tabs

One region, several panels, one visible at a time — chosen from a tab list that is a single tab stop with arrow-key movement, per the APG tabs pattern.

Basic usage

Install the package:

pnpm add @design-schema/react @design-schema/tokens

Import the stylesheet once for the app, then the component:

import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Tabs } from '@design-schema/react';

Examples

Overview panel.
import type { ReactElement } from 'react';
import { Tabs, TabPanel, type TabsItem } from '@design-schema/react';

const TABS: TabsItem[] = [
  { id: 'overview', label: 'Overview' },
  { id: 'activity', label: 'Activity', badge: '3' },
  { id: 'files', label: 'Files', icon: 'external' },
  { id: 'settings', label: 'Settings', disabled: true },
];

function panelsFor(tabs: TabsItem[]): ReactElement[] {
  return tabs.map((tab) => (
    <TabPanel key={tab.id} id={tab.id}>
      {tab.label} panel.
    </TabPanel>
  ));
}

<Tabs tabs={TABS} label="Project sections">{panelsFor(TABS)}</Tabs>

Props

9 rows
PropTypeDefaultDescriptionPlatforms
tabs requiredarray { id: string; label: string; icon?: IconName | undefined; disabled?: boolean | undefined; badge?: string | undefined }[]—

The tabs in order. `badge` is a short count or status shown after the label ("3", "New"). The icon is an Icon at `size: md` (the label's size) in the tab's current foreground color (currentColor on web/Lit, the Icon `color` prop on RN). Optional fields also accept an explicit `undefined`; the exported item type is `TabsItem`, and a package that actually shipped `TabsTab` keeps it as a deprecated alias of `TabsItem` rather than dropping it — a package that never exported it adds nothing (Lit is the only one that had it). The alias is a type, erased at runtime, so it carries the `@deprecated` tag alone and no development warning is possible. An `id` must be a valid IDREF token (no whitespace), since tab and panel element ids are built from it; the component does not sanitize it.

web, lit, rn, swiftui
children requiredcontent—

One panel per tab, in the same order, each wrapped in the exported `TabPanel` (or `<ds-tab-panel>`) with a matching `id`. Only the selected panel is rendered unless `keepMounted`. Examples describe children in words; stories and tests build them from `args.tabs` (one TabPanel per entry, id = the tab id), so a scenario that replaces `tabs` also replaces the panels and has no orphans.

web, lit, rn, swiftui
label requiredstring—

Accessible name of the tab list ("Account sections"). Not shown visually.

web, lit, rn, swiftui
valuestring—

Controlled selected tab id. Omit for uncontrolled.

web, lit, rn, swiftui
defaultValuestring—

Initially selected tab id. Defaults to the first enabled tab. Taken verbatim, never corrected: one naming a disabled tab selects it and shows its panel, one matching no tab selects nothing — in both cases the roving tab stop falls back to the first enabled tab, and neither warns.

web, lit, rn, swiftui
activationautomatic | manualautomatic

`automatic` selects a tab as arrow keys move to it (fine when panels are cheap); `manual` moves focus only and selects on Enter/Space (use when a panel loads data).

web, lit, rn, swiftui
orientationhorizontal | verticalhorizontal

Vertical tab lists sit beside their panels and use Up/Down arrows.

web, lit, rn, swiftui
fitstart | fillstart

`start` packs tabs at the start; `fill` stretches them across the width (phones, two to four tabs). Horizontal only: vertical tabs always span the list's inline size, so `fill` has no effect when vertical. A `fill` list does not scroll — it is for the few tabs that fit — so tabs that overflow it are simply squeezed, and keeping the selected tab in view is a no-op there.

web, lit, rn, swiftui
keepMountedbooleanfalse

Keep unselected panels in the tree (hidden) so their state survives switching. No effect on Lit, where panels are the consumer's light-DOM children and are always kept (hidden); the property exists there for parity and is not reflected.

web, lit, rn, swiftui

Events

1 row
EventTypeDescriptionPlatforms
onChangeonChange / change

Fired when the selected tab changes, with the new id.

web, lit, rn, swiftui

Accessibility

Decisions this component leaves to you

Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.

Implicit ARIA role: tablist.

Anatomy

tablisttabtabLabeltabIcontabBadgeindicatorpanel

What generation guarantees

Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.

Selected state — Selection is exposed programmatically (`aria-selected` / `aria-checked`).

Arrow navigation — Arrow keys move between items; Home and End jump to the ends.

Roving tabindex — The group is a single tab stop; the arrow keys move within it.

Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.

Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.

Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.

Target size — Interactive targets are at least 24×24 CSS pixels (WCAG 2.2 Target Size, minimum).

Reduced motion — Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.

Tabs keyboard support

9 rows
KeyAction
TabMoves focus to the selected tab, then out of the tab list into the panel (the list is one tab stop).
ArrowRightMoves to the next tab, wrapping; selects it under automatic activation. In a right-to-left layout ArrowRight is "previous" and ArrowLeft "next" — the direction is read from the list's (on Lit the host's) computed `direction` at keydown, as in SegmentedControl. (horizontal)
ArrowLeftMoves to the previous tab, wrapping; selects it under automatic activation. Swapped with ArrowRight in a right-to-left layout. (horizontal)
ArrowDownMoves to the next tab, wrapping; selects it under automatic activation. (vertical)
ArrowUpMoves to the previous tab, wrapping; selects it under automatic activation. (vertical)
ArrowRightFrom the last tab wraps to the first. (horizontal)
HomeFirst tab; selects it under automatic activation.
EndLast tab; selects it under automatic activation.
Enter or SpaceSelects the focused tab (manual activation). (manual)

Contrast pairs

4 rows
ForegroundBackgroundLevel
color.foreground.mutedcolor.backgroundAA
color.foreground.strongcolor.backgroundAA
color.foreground.strongcolor.background.subtleAA
color.control.selectedBackgroundcolor.backgroundAA (non-text)

Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.