Tabs
A hand-drawn tabbed interface following the WAI-ARIA Tabs pattern.
Overview
Tabs let people switch between related panels of content in the same space. One tab is active at a
time; its panel is shown while the others are hidden.
Anatomy
A role="tablist" of role="tab" buttons above role="tabpanel" regions. The active tab is filled
with a sketchy box (@ghds/sketch-core) and shown in the emphasised text colour; its panel is
associated via aria-controls/aria-labelledby. The API is data-driven — pass an items array
of { value, label, content }.
Variants & States
| State | Status | Notes |
|---|---|---|
| Selected | Implemented | Active tab: filled box + emphasised label + aria-selected. |
| Disabled | Implemented | Per-item; skipped by keyboard navigation. |
| Focus | Implemented | Roving tabindex — focus lives on the active tab. |
| Hover | Implemented | — |
Usage
Do
- Keep tab labels short and parallel in phrasing.
- Use tabs for alternate views of peer content, not for sequential steps.
- Keep it controlled if the active tab must stay in sync with other state (value + onValueChange).
Don't
- Don't use tabs when all content should be visible at once — use sections instead.
- Don't nest tabs within tabs; it gets confusing quickly.
Accessibility
- React / Web Components: full WAI-ARIA Tabs pattern —
tablist/tab/tabpanelroles,aria-selected,aria-controls/aria-labelledby, and roving tabindex (only the active tab is in the tab order). Arrow keys / Home / End move and activate tabs (automatic activation). Web Components dispatches avalue-changeevent. - React Native: tabs are
role="tab"withaccessibilityState.selected; selection is by tap. Arrow-key roving navigation is a known gap on React Native (noonKeyDownonPressable) — it is available on the React and Web-Component builds.
| Key | Action |
|---|---|
Arrow keys | Move to and activate the previous/next tab (wraps). |
Home / End | Activate the first / last tab. |
Tab | Move focus out of the tablist to the active panel. |
Content
A short label per tab and the panel content. Panel content is a ReactNode on React/React Native;
on Web Components it is panel text (content string) — for rich markup, render your own panel and
drive it from the value-change event.
Props API
React@ghds/react
| Prop | Type | Default | Description |
|---|---|---|---|
items | TabItem[] | — | { value, label, content, disabled? } per tab. |
value | string | — | Controlled active value. |
defaultValue | string | — | Initial value when uncontrolled (defaults to first). |
onValueChange | (value: string) => void | — | Fires when the active tab changes. |
label | string | — | Accessible name for the tablist. |
Web Components@ghds/web-components
| Prop | Type | Default | Description |
|---|---|---|---|
items | GhTabItem[] | — | { value, label, content?: string, disabled? } (property). |
value | string | — | Controlled active value. |
label | string | — | Accessible name for the tablist. |
value-change event | CustomEvent<string> | — | Dispatched with the new value. |
React Native@ghds/react-native
| Prop | Type | Default | Description |
|---|---|---|---|
items | TabItem[] | — | { value, label, content, disabled? } per tab. |
value | string | — | Controlled active value. |
defaultValue | string | — | Initial value when uncontrolled. |
onValueChange | (value: string) => void | — | Fires when the active tab changes. |
label | string | — | Accessible name. |
testID | string | — | Test handle for queries. |
Code examples
// React — @ghds/react
import { Tabs } from '@ghds/react/tabs';
<Tabs
label="Product details"
items={[
{ value: 'overview', label: 'Overview', content: <p>The overview panel.</p> },
{ value: 'specs', label: 'Specs', content: <p>The specifications panel.</p> },
]}
/>;
<!-- Web Components — @ghds/web-components -->
<script type="module">
import '@ghds/web-components/tabs';
</script>
<gh-tabs id="my-tabs" label="Product details"></gh-tabs>
<script>
const el = document.getElementById('my-tabs');
el.items = [
{ value: 'overview', label: 'Overview', content: 'The overview panel.' },
{ value: 'specs', label: 'Specs', content: 'The specifications panel.' },
];
el.addEventListener('value-change', (e) => { el.value = e.detail; });
</script>
// React Native — @ghds/react-native
import { Tabs } from '@ghds/react-native/tabs';
import { Text } from '@ghds/react-native/theme';
<Tabs
label="Product details"
items={[
{ value: 'overview', label: 'Overview', content: <Text>Overview</Text> },
{ value: 'specs', label: 'Specs', content: <Text>Specs</Text> },
]}
/>;