Tabs
Tabs organize content into separate views where only one view is visible at a time.
Ready to use
The Tabs component lets users switch between different views or sections of content within the same page, without navigating away. It organises related information in a compact, accessible way — improving content discoverability and reducing cognitive load. Tabs work best when content is related but needs to be divided into smaller, manageable sections such as product details, settings, or data panels.
The same visual carries two semantics. As a tab panel it switches content, as described above. As a tab picker it becomes a single-select control — the user picks one value from a small set of mutually exclusive options (for example a battery size), and nothing is navigated. The design is identical; what differs is the meaning. Everything on this page applies to both unless a section calls out the difference.
Anatomy
Section titled “Anatomy”- Tabs
- Label
When to use it
Section titled “When to use it”The choice between the two semantics comes first: is the user navigating content (tab panel) or choosing a value (tab picker)?
Tab panel
Section titled “Tab panel”Use a tab panel when:
- You need to organise related content into separate sections within the same context.
- Users should be able to switch between different views without leaving the page.
- The pattern improves the clarity of complex or lengthy interfaces.
Avoid a tab panel when:
- The sections aren’t closely related, or users need to compare content side by side.
- There are too many tabs — navigation becomes overwhelming.
Tab picker
Section titled “Tab picker”Use a tab picker when:
- Users pick one value from a small set of mutually exclusive choices.
- The options are comparable values that read well in a compact horizontal row.
- The current choice should stay visible without opening a dropdown.
Avoid a tab picker when:
- More than one option can be selected at once.
- Selecting an option should reveal or switch a content panel — that’s a tab panel.
- There are many options, or users can enter a custom value — use a dropdown or field.
- The options are unrelated or don’t belong to the same decision.
Keep it to a small set (up to ~5 options), keep labels short and of similar length, and make sure exactly one option is selected at all times.
Properties
Section titled “Properties”Scheme
Neutral or Contrast. The container picks up the scheme tokens so the tab item colours flip when the surrounding surface is dark — no per-tab overrides needed.
Variant
Two visual variants: Default (no fill behind active tab) and Filled (contrast fill on the active tab). Default suits most cases — reserve the filled variant for specific scenarios where the fill is needed (for example EV Tools like the Charging Time Calculator).
State
Rested, Hover, Active, and Disabled. The active state is reinforced with a bolder weight and a coloured border, so the cue isn’t carried by colour alone.
EV accent
A special border colour for the active tab when the Tabs sit inside an EV-tools surface. Use only for EV Tools — not as a general accent.
Platform considerations
Section titled “Platform considerations”Desktop
Tabs sit in a single row at the top of the panel. Keep labels short enough to fit on one line so the comparison reads at a glance.
Tablet
Same row layout. Make sure the tab row doesn’t overflow horizontally — if labels grow, consider shorter wording or a different navigation pattern.
Mobile
Tabs can scroll horizontally when there’s no room for the full set. Keep each label short so the visible portion still names the section.
Best practices
Section titled “Best practices”Use ARIA semantics so keyboard, focus, and assistive-tech behaviour come for free.
Do
Keep tab labels short, clear, and descriptive; ensure exactly one tab is selected at a time; provide visible focus indicators for keyboard users; use proper heading hierarchy within panels; pair an active visual with bold text or an underline so the cue isn’t colour-only; use the default no-fill variant in most cases and reserve the filled variant for EV Tools.
Don't
Don’t mimic tab behaviour with <div> elements that lack ARIA roles, don’t allow multiple tabs to be selected at once, don’t remove inactive panels from the DOM (use hidden instead so accessibility holds), don’t auto-shift focus to the panel on tab switch (keep focus on the tab), don’t rely on colour alone to indicate the active tab, don’t let labels wrap or truncate, and don’t use tabs for unrelated or excessively long content (split it into separate pages).
Content guidelines
Section titled “Content guidelines”Tab labels should name the section they reveal, in one or two words (“Overview”, “Specifications”, “Reviews”). Keep wording parallel across the row (all nouns or all verbs) so the comparison reads coherently, use sentence case, and avoid trailing punctuation. Each panel’s first heading should match or echo the tab label so screen-reader users find their bearings when focus moves into the content.
Styles
Section titled “Styles”The tab strip carries two semantics over one shared visual (see the Accessibility tab for the roles and keyboard model):
- Tab panel — WAI-ARIA tabs: content-switching navigation where one panel is
visible at a time. The item is a
<button role="tab">. - Tab picker — a single-select control (a radio group) for choosing one value
from a small set of mutually exclusive options. The item is a
<label>wrapping a native radio. Not navigation.
The visual variants (EV, Filled) apply to both semantics.
Tab panel
Section titled “Tab panel”Tabs organize content into separate views where only one view is visible at a time.
<div class="tng-tabs"> <button class="is-selected">Tab 1</button> <button>Tab 2</button> <button>Tab 3</button></div>Variants
Section titled “Variants”<button class="tng-tabs-item is-ev">EV</button><button class="tng-tabs-item is-ev is-selected">EV Selected</button>Filled
Section titled “Filled”<button class="tng-tabs-item is-filled">Filled</button><button class="tng-tabs-item is-filled is-selected"> Filled Selected</button><button class="tng-tabs-item is-filled is-ev is-selected"> EV Filled Selected</button>States
Section titled “States”Disabled
Section titled “Disabled”<button class="tng-tabs-item" disabled>Default</button><button class="tng-tabs-item is-filled" disabled>Filled</button>Selected
Section titled “Selected”Use .is-selected or aria-selected="true" to indicate the currently selected tab item.
Advanced structure
Section titled “Advanced structure”You can compose our existing layouts and utility classes to create more complex tab items, such as adding images.
By default a selected tab will have all its text in bold. If you want to limit the bold text to a single element, wrap said element in .tng-tabs-item-label.
<button class="tng-tabs-item is-selected"> <div class="tng-stack align-items-center gap-sm"> <div class="align-self-start">Label</div> <div class="tng-slot is-primary"></div> <span class="tng-tabs-item-label"> Overview <span class="fg-primary-default">1</span> </span> </div></button>Overflow
Section titled “Overflow”You can use the overflow utility to wrap the tabs container and enable horizontal scrolling when there are too many tabs to fit in the available space.
<div class="tng-overflow-scroll-indicators" data-overflow-start="true" data-overflow-end="true"> <i class="tng-icon icon-chevron-left" aria-hidden="true"></i> <div class="tng-overflow-scroll has-hidden-scrollbar is-inline" style="max-inline-size: 250px" > <div class="tng-tabs"> <button class="tng-tabs-item is-selected">Tab 1</button> <button class="tng-tabs-item">Tab 2</button> <button class="tng-tabs-item">Tab 3</button> <button class="tng-tabs-item">Tab 4</button> <button class="tng-tabs-item">Tab 5</button> </div> </div> <i class="tng-icon icon-chevron-right" aria-hidden="true"></i></div>Recipe
Section titled “Recipe”Overview content goes here.
Features content goes here.
Performance content goes here.
Specifications content goes here.
Safety content goes here.
Warranty content goes here.
Charging content goes here.
Tab picker
Section titled “Tab picker”A single-select control that borrows the tab visual. It’s a native radio group
— a <fieldset> of <input type="radio"> sharing one name — so single-select,
group labelling, and arrow-key roving all come from the browser, with no JS and
no ARIA.
Wrap each radio in a .tng-tabs-item <label>. The styleguide keys the selected,
disabled, and focus looks off the nested radio (:has(input:checked) /
:has(input:disabled) / :has(input:focus-visible)) and visually hides the radio
itself — so the markup needs no extra classes and no consumer CSS. The design has
no visible group label, but the <fieldset> still needs a <legend> for its
accessible name, so hide it with sr-only. The
.tng-stack fieldset keeps the strip sized to its content.
Reach for it with a small set of mutually exclusive values (up to ~5); for content-switching navigation use a tab panel instead.
<fieldset class="tng-stack"> <legend class="sr-only">Battery</legend> <div class="tng-tabs"> <label class="tng-tabs-item is-ev"> <input type="radio" name="battery" value="50" /> 50 KWH </label> <label class="tng-tabs-item is-ev"> <input type="radio" name="battery" value="75" checked /> 75 KWH </label> <label class="tng-tabs-item is-ev"> <input type="radio" name="battery" value="90" disabled /> 90 KWH </label> </div></fieldset>Import the Tabs component from @tmedxp/react-components.
Properties
Section titled “Properties”TabsProperties extends ComponentProps<'div'> (excluding onChange and className), meaning it includes all standard HTML attributes that can be applied to a <div>.
| Prop | Type | Description | Required |
|---|---|---|---|
items |
TabItemData[] |
Array of tab items to render. | ✅ |
tabListAriaLabel |
string |
Accessible label for the tablist container. | ✅ |
defaultValue |
string |
Initially selected tab value (uncontrolled mode). | |
activeValue |
string |
Currently selected tab value (controlled mode). | |
onChange |
(value: string) => void |
Callback fired when the selected tab changes. | |
variant |
'default' | 'filled' | 'ev' | 'ev-filled' |
Visual variant. Default is 'default'. |
|
controls |
ReactNode | ((props: TabControlsRenderProperties) => ReactNode) |
Custom controls replacing the default overflow navigation. | |
hideControls |
boolean |
Explicitly hide or show overflow controls. Auto-shown when items > 3. | |
idPrefix |
string |
Prefix for generated tab/panel IDs. Falls back to useId(). |
|
className |
ClassValue |
Custom class names for the wrapper element. | |
scrollPreviousAriaLabel |
string |
Accessible label for the scroll-previous button. | |
scrollNextAriaLabel |
string |
Accessible label for the scroll-next button. |
TabItemData
Section titled “TabItemData”Each item in the items array follows this shape:
| Prop | Type | Description | Required |
|---|---|---|---|
title |
string |
Tab label text. | ✅ |
value |
string |
Unique identifier for the tab. | ✅ |
content |
ReactNode |
Panel content rendered when the tab is active. | |
label |
string |
Category label displayed above the title. | |
image |
ReactNode |
Image or media displayed in the tab item. | |
badge |
string | number |
Badge counter displayed after the title. | |
disabled |
boolean |
Disables the tab (skipped during keyboard navigation). | |
isLoading |
boolean |
Shows loading state in the panel instead of content. | |
loadingContent |
ReactNode |
Custom loading indicator (defaults to “Loading content…”). |
TabControlsRenderProperties
Section titled “TabControlsRenderProperties”When passing a render function to controls, it receives:
| Prop | Type | Description |
|---|---|---|
activeValue |
string |
Currently selected tab value. |
activeIndex |
number |
Index of the active tab among enabled items. |
enabledItems |
TabItemData[] |
Filtered list of non-disabled items. |
items |
TabItemData[] |
Full items array. |
scrollIndex |
number |
Current carousel scroll snap index. |
scrollCount |
number |
Total number of scroll snap points. |
canScrollPrevious |
boolean |
Whether scrolling backward is possible. |
canScrollNext |
boolean |
Whether scrolling forward is possible. |
selectTab |
(value: string) => void |
Select a tab by value. |
selectPreviousTab |
() => void |
Select the previous enabled tab. |
selectNextTab |
() => void |
Select the next enabled tab. |
Examples
Section titled “Examples”Basic usage
Section titled “Basic usage”import { Tabs } from '@tmedxp/react-components';
const BasicTabs = () => { const items = [ { title: 'Tab 1', value: 'tab-1', content: 'Tab 1 content goes here.' }, { title: 'Tab 2', value: 'tab-2', content: 'Tab 2 content goes here.' }, { title: 'Tab 3', value: 'tab-3', content: 'Tab 3 content goes here.' }, ];
return ( <Tabs items={items} defaultValue="tab-1" tabListAriaLabel="Example tabs" /> );};
export { BasicTabs };Advanced structure
Section titled “Advanced structure”import { Tabs } from '@tmedxp/react-components';
const AdvancedTabs = () => { const items = [ { label: 'Category', title: 'Overview', value: 'overview', badge: 1, image: <img src="/models/yaris.webp" alt="" />, content: 'Overview content goes here.', }, { label: 'Category', title: 'Features', value: 'features', badge: 3, image: <img src="/models/corolla.webp" alt="" />, content: 'Features content goes here.', }, { label: 'Category', title: 'Specifications', value: 'specifications', image: <img src="/models/camry.webp" alt="" />, content: 'Specifications content goes here.', }, ];
return ( <Tabs items={items} defaultValue="overview" tabListAriaLabel="Advanced tabs with labels, images and badge counters" /> );};
export { AdvancedTabs };With custom controls
Section titled “With custom controls”Use a render function in controls to replace the default overflow chevrons with a count indicator or any custom navigation.
import { CountIndicator, Tabs } from '@tmedxp/react-components';
const TabsWithCountIndicator = () => { const items = [ { title: 'Yaris', value: 'yaris', content: '…' }, { title: 'Corolla', value: 'corolla', content: '…' }, { title: 'Camry', value: 'camry', content: '…' }, { title: 'RAV4', value: 'rav4', content: '…' }, { title: 'Highlander', value: 'highlander', content: '…' }, ];
return ( <Tabs items={items} defaultValue="yaris" tabListAriaLabel="Toyota car models" controls={({ scrollIndex, scrollCount, selectPreviousTab, selectNextTab, }) => ( <CountIndicator index={scrollIndex} total={scrollCount} prevAriaLabel="Previous tab" nextAriaLabel="Next tab" helperOfLabel="of" onPrev={selectPreviousTab} onNext={selectNextTab} /> )} /> );};
export { TabsWithCountIndicator };EV variant
Section titled “EV variant”import { Tabs } from '@tmedxp/react-components';
const EvTabs = () => { const items = [ { title: 'Charging', value: 'charging', content: 'Charging info.' }, { title: 'Range', value: 'range', content: 'Range details.' }, { title: 'Battery', value: 'battery', content: 'Battery specs.' }, ];
return ( <Tabs items={items} defaultValue="charging" variant="ev" tabListAriaLabel="EV tools tabs" /> );};
export { EvTabs };Disabled items
Section titled “Disabled items”import { Tabs } from '@tmedxp/react-components';
const TabsWithDisabled = () => { const items = [ { title: 'Available', value: 'available', content: 'Available content.', }, { title: 'Coming soon', value: 'coming-soon', disabled: true, content: '', }, { title: 'Also available', value: 'also-available', content: 'More content.', }, ];
return ( <Tabs items={items} defaultValue="available" tabListAriaLabel="Tabs with disabled option" /> );};
export { TabsWithDisabled };Loading state
Section titled “Loading state”import { Tabs } from '@tmedxp/react-components';
const TabsWithLoading = () => { const items = [ { title: 'Loaded', value: 'loaded', content: 'Content ready.' }, { title: 'Loading', value: 'loading', isLoading: true, loadingContent: <span>Fetching content...</span>, content: 'This appears after loading completes.', }, ];
return ( <Tabs items={items} defaultValue="loaded" tabListAriaLabel="Tabs with loading state" /> );};
export { TabsWithLoading };Import the Tabs component from @tmedxp/react-components.
Properties
Section titled “Properties”TabsProperties extends ComponentProps<'div'> (excluding onChange and className), meaning it includes all standard HTML attributes that can be applied to a <div>.
| Prop | Type | Description | Required |
|---|---|---|---|
items |
TabItemData[] |
Array of tab items to render. | ✅ |
tabListAriaLabel |
string |
Accessible label for the tablist container. | ✅ |
defaultValue |
string |
Initially selected tab value (uncontrolled mode). | |
activeValue |
string |
Currently selected tab value (controlled mode). | |
onChange |
(value: string) => void |
Callback fired when the selected tab changes. | |
variant |
'default' | 'filled' | 'ev' | 'ev-filled' |
Visual variant. Default is 'default'. |
|
controls |
ReactNode | ((props: TabControlsRenderProperties) => ReactNode) |
Custom controls replacing the default overflow navigation. | |
hideControls |
boolean |
Explicitly hide or show overflow controls. Auto-shown when items > 3. | |
idPrefix |
string |
Prefix for generated tab/panel IDs. Falls back to useId(). |
|
className |
ClassValue |
Custom class names for the wrapper element. | |
scrollPreviousAriaLabel |
string |
Accessible label for the scroll-previous button. | |
scrollNextAriaLabel |
string |
Accessible label for the scroll-next button. |
TabItemData
Section titled “TabItemData”Each item in the items array follows this shape:
| Prop | Type | Description | Required |
|---|---|---|---|
title |
string |
Tab label text. | ✅ |
value |
string |
Unique identifier for the tab. | ✅ |
content |
ReactNode |
Panel content rendered when the tab is active. | |
label |
string |
Category label displayed above the title. | |
image |
ReactNode |
Image or media displayed in the tab item. | |
badge |
string | number |
Badge counter displayed after the title. | |
disabled |
boolean |
Disables the tab (skipped during keyboard navigation). | |
isLoading |
boolean |
Shows loading state in the panel instead of content. | |
loadingContent |
ReactNode |
Custom loading indicator (defaults to “Loading content…”). |
TabControlsRenderProperties
Section titled “TabControlsRenderProperties”When passing a render function to controls, it receives:
| Prop | Type | Description |
|---|---|---|
activeValue |
string |
Currently selected tab value. |
activeIndex |
number |
Index of the active tab among enabled items. |
enabledItems |
TabItemData[] |
Filtered list of non-disabled items. |
items |
TabItemData[] |
Full items array. |
scrollIndex |
number |
Current carousel scroll snap index. |
scrollCount |
number |
Total number of scroll snap points. |
canScrollPrevious |
boolean |
Whether scrolling backward is possible. |
canScrollNext |
boolean |
Whether scrolling forward is possible. |
selectTab |
(value: string) => void |
Select a tab by value. |
selectPreviousTab |
() => void |
Select the previous enabled tab. |
selectNextTab |
() => void |
Select the next enabled tab. |
Examples
Section titled “Examples”Basic usage
Section titled “Basic usage”import { Tabs } from '@tmedxp/react-components';
const BasicTabs = () => { const items = [ { title: 'Tab 1', value: 'tab-1', content: 'Tab 1 content goes here.' }, { title: 'Tab 2', value: 'tab-2', content: 'Tab 2 content goes here.' }, { title: 'Tab 3', value: 'tab-3', content: 'Tab 3 content goes here.' }, ];
return ( <Tabs items={items} defaultValue="tab-1" tabListAriaLabel="Example tabs" /> );};
export { BasicTabs };Advanced structure
Section titled “Advanced structure”import { Tabs } from '@tmedxp/react-components';
const AdvancedTabs = () => { const items = [ { label: 'Category', title: 'Overview', value: 'overview', badge: 1, image: <img src="/models/yaris.webp" alt="" />, content: 'Overview content goes here.', }, { label: 'Category', title: 'Features', value: 'features', badge: 3, image: <img src="/models/corolla.webp" alt="" />, content: 'Features content goes here.', }, { label: 'Category', title: 'Specifications', value: 'specifications', image: <img src="/models/camry.webp" alt="" />, content: 'Specifications content goes here.', }, ];
return ( <Tabs items={items} defaultValue="overview" tabListAriaLabel="Advanced tabs with labels, images and badge counters" /> );};
export { AdvancedTabs };With custom controls
Section titled “With custom controls”Use a render function in controls to replace the default overflow chevrons with a count indicator or any custom navigation.
import { CountIndicator, Tabs } from '@tmedxp/react-components';
const TabsWithCountIndicator = () => { const items = [ { title: 'Yaris', value: 'yaris', content: '…' }, { title: 'Corolla', value: 'corolla', content: '…' }, { title: 'Camry', value: 'camry', content: '…' }, { title: 'RAV4', value: 'rav4', content: '…' }, { title: 'Highlander', value: 'highlander', content: '…' }, ];
return ( <Tabs items={items} defaultValue="yaris" tabListAriaLabel="Toyota car models" controls={({ scrollIndex, scrollCount, selectPreviousTab, selectNextTab, }) => ( <CountIndicator index={scrollIndex} total={scrollCount} prevAriaLabel="Previous tab" nextAriaLabel="Next tab" helperOfLabel="of" onPrev={selectPreviousTab} onNext={selectNextTab} /> )} /> );};
export { TabsWithCountIndicator };EV variant
Section titled “EV variant”import { Tabs } from '@tmedxp/react-components';
const EvTabs = () => { const items = [ { title: 'Charging', value: 'charging', content: 'Charging info.' }, { title: 'Range', value: 'range', content: 'Range details.' }, { title: 'Battery', value: 'battery', content: 'Battery specs.' }, ];
return ( <Tabs items={items} defaultValue="charging" variant="ev" tabListAriaLabel="EV tools tabs" /> );};
export { EvTabs };Disabled items
Section titled “Disabled items”import { Tabs } from '@tmedxp/react-components';
const TabsWithDisabled = () => { const items = [ { title: 'Available', value: 'available', content: 'Available content.' }, { title: 'Coming soon', value: 'coming-soon', disabled: true, content: '' }, { title: 'Also available', value: 'also-available', content: 'More content.', }, ];
return ( <Tabs items={items} defaultValue="available" tabListAriaLabel="Tabs with disabled option" /> );};
export { TabsWithDisabled };Loading state
Section titled “Loading state”import { Tabs } from '@tmedxp/react-components';
const TabsWithLoading = () => { const items = [ { title: 'Loaded', value: 'loaded', content: 'Content ready.' }, { title: 'Loading', value: 'loading', isLoading: true, loadingContent: <span>Fetching content...</span>, content: 'This appears after loading completes.', }, ];
return ( <Tabs items={items} defaultValue="loaded" tabListAriaLabel="Tabs with loading state" /> );};
export { TabsWithLoading };The Tabs component follows the WAI-ARIA Tabs pattern — a tablist whose tabs each reveal a panel — and meets WCAG 2.1 AA. The same strip carries two semantics: as a tab panel it switches content and uses the Tabs pattern; as a tab picker it is a single-select control built from a native radio group, so single-select, group labelling, and arrow-key roving all come from the browser with no ARIA. An accessible tabs strip stays reachable and operable from the keyboard, keeps assistive technology informed of which tab is selected and which panel it controls, and gives every element an accessible name — so the current view is clear whether the user is looking at it, listening to it, or tabbing through it.
For designers
Section titled “For designers”- All states (rested, hover, active, disabled) must maintain accessible contrast on every supported surface. Source: WCAG 1.4.3 Contrast (Minimum).
- Selection must never rely on colour alone — reinforce it with the emphasised label weight and the active border, so it survives for colour-blind users. Source: WCAG 1.4.1 Use of Color.
- Focus styles must stay visible, meet contrast requirements on every supported surface, and be clearly distinguishable from hover. Source: WCAG 2.4.7 Focus Visible.
- Use semantic colour tokens (e.g.
foreground/neutral/default) rather than fixed values, so the component adapts correctly across themes and surfaces.
For developers
Section titled “For developers”Pick the pattern by what the component means, not how it looks — a tab panel that navigates content, or a tab picker that chooses one value.
A tab panel follows the WAI-ARIA Tabs pattern: a tablist of tab buttons, each revealing its own panel, with inactive panels kept in the DOM under the hidden attribute so the tab-to-panel relationship survives. The recipe uses auto-activation — moving to a tab with the arrow keys selects it and reveals its panel in one step — and keeps focus on the tab rather than shifting it into the panel.
A tab picker is a radio group, not a tablist — native radios sharing one name. It needs no ARIA and no JS: single-select, roving focus, and the group all come from the browser. Do not add role="tablist", role="tab", or role="radiogroup" — the native fieldset and radios already expose the group. The radio is visually hidden but stays focusable and present in the accessibility tree, so it announces as “radio button, n of m, group name”.
The per-element roles, names, and states are in the Labelling elements section below.
Keyboard interaction
Section titled “Keyboard interaction”Both semantics are fully operable from the keyboard. Source: WCAG 2.1.1 Keyboard.
Tab panel (WAI-ARIA tablist, auto-activation):
| Key | Action |
|---|---|
| ← / → | Move to and activate the previous / next tab |
| Home / End | Move to and activate the first / last tab |
| Enter / Space | Activate the focused tab |
| Tab | Move focus out of the tablist to the next control |
Tab picker (native radio group):
| Key | Action |
|---|---|
| ← / ↑ | Move to and select the previous option (native radio roving) |
| → / ↓ | Move to and select the next option |
| Tab | Move focus into the group (the checked option) / out of the group |
| Space | Select the focused option |
Interactive targets must be large enough for comfortable touch use, comfortably clearing the WCAG AA minimum. Source: WCAG 2.5.8 Target Size (Minimum).
Focus order
Section titled “Focus order”Tab lands on the focused tab — a roving tabindex keeps only that one tab in the page tab sequence while the arrow keys rove between the others — then a further Tab leaves the tablist for the next control. The tab picker gets the same single-stop-then-rove behaviour for free from its native radio group. The keys that drive this are in the Keyboard interaction table above.
Overview content goes here.
- Focused tab
Source: WCAG 2.4.3 Focus Order.
Labelling elements
Section titled “Labelling elements”Give every element the role, name, and state assistive technology needs.
Tab list
role="tablist" on the strip. Name it with aria-label (the recipe uses aria-label="Content tabs") or aria-labelledby, so it doesn’t announce as an unnamed group.
Tab
role="tab" on each <button>, with aria-selected kept in sync (true on the selected tab, false on the rest) and aria-controls pointing at the panel it reveals. Each tab takes its accessible name from its text content — keep labels short and self-describing.
Tab panel
role="tabpanel", named back to its tab with aria-labelledby, and hidden while inactive. Give it tabindex="0" when it holds no focusable content, so keyboard users can still reach the panel.
Tab picker group
The native <fieldset> exposes the radio group; its <legend> is the group’s accessible name — hide it with sr-only when the design shows no visible label. Each option’s name comes from its <label> text, and the checked radio is the selection, exposed through :checked rather than aria-selected.
Source: WCAG 4.1.2 Name, Role, Value.