Skip to content

Develop

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.

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>
<button class="tng-tabs-item is-ev">EV</button>
<button class="tng-tabs-item is-ev is-selected">EV Selected</button>
<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>
<button class="tng-tabs-item" disabled>Default</button>
<button class="tng-tabs-item is-filled" disabled>Filled</button>

Use .is-selected or aria-selected="true" to indicate the currently selected tab item.

<button class="tng-tabs-item is-selected">Selected Tab</button>

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>

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>

Overview content goes here.

source code

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>
Battery

Import the Tabs component from @tmedxp/react-components.

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.

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…”).

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

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

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.

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…”).

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

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