Tab Picker
Selection – Input
A tab picker lets users choose one value from a small set of mutually exclusive options, laid out as a tab-like strip. It looks like Tabs, but it navigates nothing — it is a single-select control, and only one option is selected at a time.
Ready to use
The Tab Picker lets users select a single option from a predefined set of mutually exclusive choices, displayed in a tab-like layout. Although it is visually identical to Tabs, it does not control navigation and does not switch between content sections — it is a single-selection control that lets users choose one value from a small set. Reach for it where people need to compare and pick between related values quickly (a battery size, a fuel type) while keeping a compact, familiar visual pattern. Only one option can be selected at a time.
Anatomy
Section titled “Anatomy”The picker is a group of options over the shared tab strip. Exactly one option carries the selected treatment; the group’s label is usually implied by the surrounding content rather than shown.
- Tab picker
- Option
- Selected option
When to use it
Section titled “When to use it”The question that separates a tab picker from Tabs is what the click does: a tab picker chooses a value, Tabs switches a content panel. The two look the same, so pick by meaning rather than by appearance.
Use a tab picker when:
- Users select one option from a small set of mutually exclusive choices.
- The options are comparable values that read well in a compact horizontal row.
- The currently selected option should be clearly indicated.
- Users switch quickly between predefined values.
- Frequently used selections 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 hide a content panel — use Tabs.
- There are many options, or users can enter a custom value — use a dropdown or a field.
- The options are unrelated or don’t belong to the same decision group.
Keep it to a small set — the design caps it at five options — 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 option colours flip when the surrounding surface is dark — no per-option overrides needed.
EV accent
A special border colour for the selected option when the picker sits inside an EV-tools surface — the Charging Time Calculator’s battery and fuel-type pickers are the reference case. Use only for EV Tools, not as a general accent.
State
Rested, Hover, Active, Focus, and Disabled. The active state is reinforced with a bolder weight, a coloured border and a contrast fill, so the cue isn’t carried by colour alone, and focus stays distinguishable from hover.
Options
Up to five mutually exclusive values, with exactly one selected at any time. Labels of similar length keep the row balanced; the picker has no overflow behaviour, because a set that needs scrolling is too large for this control.
The selected option carries the same contrast fill as a selected tab, painted from the selection itself — there is nothing to set per option.
Platform considerations
Section titled “Platform considerations”Desktop
The options sit in a single row next to the value they qualify. Keep labels short enough to fit on one line so the whole set can be compared at a glance.
Tablet
Same row layout. With five options as the ceiling the row should always fit — if it doesn’t, the wording is too long or the set is too large for a picker.
Mobile
Still a single row: unlike Tabs, the picker doesn’t scroll horizontally. Shorten the labels or move to a dropdown rather than letting the set overflow.
Best practices
Section titled “Best practices”The picker’s whole job is to make one choice out of a few obvious, and to keep that choice visible afterwards.
Do
Keep labels short, clear, and easy to scan; limit the set to a maximum of five options; ensure only one option can be selected at any time; use it for related values belonging to the same decision group; give rested, hover, active, focus, and disabled a clear visual distinction; keep option lengths consistent wherever possible; keep the selected option visible and identifiable at all times; and place the picker close to the information it relates to.
Don't
Don’t use it for navigation between pages or content sections, don’t allow multiple options to be selected simultaneously, don’t use it for large option sets that become hard to scan, don’t truncate meaningful labels, don’t rely exclusively on colour to indicate the selected state, don’t mix unrelated options within the same picker, and don’t use it as a replacement for Tabs when content panels are required.
Content guidelines
Section titled “Content guidelines”Option labels name the value they select, not an action — “50 kWh”, “Petrol”, “Monthly”. Keep the wording parallel across the row (all units, all nouns) and of similar length so the options read as one comparable set, use sentence case, and avoid trailing punctuation. Where the values share a unit, repeat it on each option rather than hiding it in surrounding copy, so each label stands on its own when read out in isolation.
A tab picker is a native radio group wearing the tab strip’s visual — 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. It reuses Tabs’ .tng-tabs / .tng-tabs-item classes rather than shipping its own: the design is identical, and only the nested markup differs.
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 five); for content-switching navigation use Tabs instead.
<fieldset class="tng-stack"> <legend class="sr-only">Battery</legend> <div class="tng-tabs"> <label class="tng-tabs-item"> <input type="radio" name="battery" value="50" /> 50 KWH </label> <label class="tng-tabs-item"> <input type="radio" name="battery" value="75" checked /> 75 KWH </label> <label class="tng-tabs-item"> <input type="radio" name="battery" value="90" /> 90 KWH </label> </div></fieldset>Variants
Section titled “Variants”Add .is-ev to each option to switch the selected option’s border to the EV accent. Use it only on EV-tools surfaces.
<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-ev" value="50" /> 50 KWH </label> <label class="tng-tabs-item is-ev"> <input type="radio" name="battery-ev" value="75" checked /> 75 KWH </label> <label class="tng-tabs-item is-ev"> <input type="radio" name="battery-ev" value="90" /> 90 KWH </label> </div></fieldset>States
Section titled “States”Selected
Section titled “Selected”The checked radio is the selection — mark it with the native checked attribute. There is no .is-selected class to keep in sync, because .tng-tabs-item:has(input:checked) reads the state straight off the radio.
<label class="tng-tabs-item"> <input type="radio" name="selected-demo" value="75" checked /> Selected option</label>Disabled
Section titled “Disabled”Disable the nested radio, not the label. .tng-tabs-item:has(input:disabled) picks up the disabled look, and the browser keeps the option out of the group’s arrow-key roving.
Import the TabPicker component from @tmedxp/react-components.
Properties
Section titled “Properties”TabPickerProperties extends HTMLAttributes<HTMLDivElement> (excluding onChange and className), meaning it includes all standard HTML attributes that can be applied to a <div>.
| Prop | Type | Description | Required |
|---|---|---|---|
items |
TabPickerItemData[] |
Options rendered by the picker. | ✅ |
label |
string |
Group label shown above the picker and used as aria-label. |
|
defaultValue |
string |
Initially selected value (uncontrolled mode). | |
activeValue |
string |
Currently selected value (controlled mode). | |
onChange |
(value: string) => void |
Callback fired when the selected value changes. | |
variant |
'default' | 'filled' | 'ev' | 'ev-filled' |
Visual variant. Default is 'default'. |
|
className |
ClassValue |
Custom class names for the wrapper element. |
The React implementation renders at most 4 options. When more than 4 items are passed, only the first 4 are rendered.
TabPickerItemData
Section titled “TabPickerItemData”Each item in the items array follows this shape:
| Prop | Type | Description | Required |
|---|---|---|---|
title |
string |
Option label text. | ✅ |
value |
string |
Unique identifier for the option. | ✅ |
disabled |
boolean |
Disables the option. |
Examples
Section titled “Examples”Basic usage
Section titled “Basic usage”import { TabPicker } from '@tmedxp/react-components';
const BasicTabPicker = () => { return ( <TabPicker label="Battery" defaultValue="75kwh" items={[ { title: '50 kWh', value: '50kwh' }, { title: '75 kWh', value: '75kwh' }, { title: '100 kWh', value: '100kwh' }, ]} /> );};Controlled value
Section titled “Controlled value”import { useState } from 'react';import { TabPicker } from '@tmedxp/react-components';
const ControlledTabPicker = () => { const [value, setValue] = useState('hybrid');
return ( <> <TabPicker label="Powertrain" activeValue={value} onChange={setValue} items={[ { title: 'Hybrid', value: 'hybrid' }, { title: 'Plug-in Hybrid', value: 'phev' }, { title: 'Battery Electric', value: 'bev' }, ]} /> <p>Selected value: {value}</p> </> );};EV filled variant
Section titled “EV filled variant”import { TabPicker } from '@tmedxp/react-components';
const EvFilledTabPicker = () => { return ( <TabPicker label="Wheels" variant="ev-filled" items={[ { title: '18"', value: '18' }, { title: '19"', value: '19' }, { title: '20"', value: '20' }, { title: '21"', value: '21' }, ]} /> );};With disabled options
Section titled “With disabled options”import { TabPicker } from '@tmedxp/react-components';
const TabPickerWithDisabled = () => { return ( <TabPicker label="Battery" variant="filled" items={[ { title: '50 kWh', value: '50kwh' }, { title: '75 kWh', value: '75kwh', disabled: true }, { title: '100 kWh', value: '100kwh' }, ]} /> );};The Tab Picker is a radio group, not a tablist — it follows the WAI-ARIA Radio Group pattern and meets WCAG 2.1 AA. It borrows the tab strip’s visual from Tabs, but nothing about its accessibility comes from that resemblance: an accessible picker announces itself as a named group of mutually exclusive options, exposes exactly one of them as checked, and stays fully operable from the keyboard — so the current choice 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, focus, and 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 and from the selected state. Source: WCAG 2.4.7 Focus Visible.
- Disabled options must stay readable while clearly reading as unavailable.
- 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”Build the picker from a native <fieldset> of <input type="radio"> elements sharing one name. It needs no ARIA and no JS: single-select, roving focus, and the group itself all come from the browser. Do not add role="tablist", role="tab", or role="radiogroup" — the native fieldset and radios already expose the group, and a tablist role would promise content panels the picker doesn’t have. Each 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”The picker is fully operable from the keyboard, and every key below is native radio-group behaviour rather than something the consumer wires up. Source: WCAG 2.1.1 Keyboard.
| Key | Action |
|---|---|
| Tab | Move focus into the group (the checked option), then out again |
| Shift + Tab | Move focus to the previous interactive element |
| ← / ↑ | Move to and select the previous option |
| → / ↓ | Move to and select the next option |
| Space | Select the focused option |
Because the arrow keys select as they move, a picker should never be wired so that changing the selection navigates or submits — that would strand keyboard users mid-row.
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”The whole group is a single stop in the page’s tab sequence: Tab lands on the checked option (or the first option when none is checked), and a further Tab leaves the group entirely — the arrow keys rove between options from there. The keys that drive this are in the Keyboard interaction table above.
- Focused option
Source: WCAG 2.4.3 Focus Order.
Labelling elements
Section titled “Labelling elements”Give every element the role, name, and state assistive technology needs.
Group
The native <fieldset> exposes the radio group — no role is needed or wanted. Its <legend> is the group’s accessible name, and it is required even though the design shows no visible label: hide it with sr-only rather than dropping it, or the options announce with no indication of what they belong to.
Option
Each <input type="radio"> takes its accessible name from the <label> text wrapping it, so no aria-label is needed — keep labels short and self-describing. All options in one picker must share a single name attribute; that attribute is what makes them mutually exclusive.
Selected state
The checked radio is the selection, exposed through native :checked rather than aria-selected or aria-current. Ship exactly one option checked in the initial markup so the group never announces as having no value.
Disabled option
Disable the nested <input> with the native disabled attribute, not the <label> and not an .is-disabled class. The browser then removes the option from the group’s arrow-key roving and announces it as unavailable.
Source: WCAG 4.1.2 Name, Role, Value.