Develop
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 };