tab-set
Tabbed content panels with keyboard navigation and ARIA semantics.
Overview
The <tab-set> component transforms a group of <details> elements into a tab interface. Without JavaScript, it works as an accordion. With JavaScript, it adds keyboard navigation and ARIA linkage attributes while preserving native <details>/<summary> semantics.
<tab-set aria-label="Feature tabs"> <details name="feature-tabs" open> <summary>Overview</summary> <div>Content for Overview tab</div> </details> <details name="feature-tabs"> <summary>Features</summary> <div>Content for Features tab</div> </details> <details name="feature-tabs"> <summary>Usage</summary> <div>Content for Usage tab</div> </details></tab-set>
Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
aria-label | string | — | Accessible label for the tab group |
transition | "fade", "slide", "scale" | — | View Transition animation between tab panels |
Required Structure
| Element | Required | Description |
|---|---|---|
<details> | yes | One per tab — native disclosure element provides open/close state |
<summary> | yes | Tab label inside each <details> |
Child Details Attributes
| Attribute | Type | Description |
|---|---|---|
name |
string | Shared name for exclusive behavior. All tabs should have the same name. |
open |
boolean | Initially selected tab. If none specified, first tab is selected. |
Progressive Enhancement
The component is built on the native <details> element, which provides built-in expand/collapse functionality. The shared name attribute (supported in modern browsers) ensures only one panel is open at a time.
Without JavaScript
When JavaScript is disabled or fails to load, the tabs degrade gracefully to an accordion-style interface where each panel can be expanded independently.
With JavaScript
- Native
<details>/<summary>semantics are preserved (no role overrides) aria-controlsandaria-labelledbylink each tab to its panelaria-selectedstate is managed on each summarytabindexis managed for roving focus- Keyboard navigation (arrow keys, Home, End) is enabled
Keyboard Navigation
When focused on a tab, the following keyboard shortcuts are available:
| Key | Action |
|---|---|
| ArrowRight | Move to and activate the next tab |
| ArrowLeft | Move to and activate the previous tab |
| Home | Move to and activate the first tab |
| End | Move to and activate the last tab |
Try using arrow keys to navigate between tabs:
First
Press ArrowRight to move to the next tab.
Second
Press ArrowLeft to move to the previous tab, or End to jump to the last.
Third
Press Home to jump back to the first tab.
Rich Tab Content
Tabs can contain any HTML content, including forms, images, and other components.
Text Content
Tabs can contain rich text content with proper typography.
Including blockquotes, lists, and other typographic elements.
Form Content
Card Grid
Events
The component dispatches a custom event when a tab is changed.
| Event | Detail | Description |
|---|---|---|
tab-set:change |
{ index: number } |
Fired when a tab is activated. index is the 0-based tab index. |
<script>const tabs = document.querySelector('tab-set'); tabs.addEventListener('tab-set:change', (event) => { console.log('Tab changed to:', event.detail.index);});</script>
Accessibility
Native Semantics
The component preserves native <details>/<summary> semantics. It does not override roles with role="tablist", role="tab", or role="tabpanel". Instead, it layers ARIA linkage attributes on top of the native elements:
aria-controlson each<summary>points to its panelaria-labelledbyon each panel points back to its summaryaria-selectedreflects which tab is currently activetabindexis managed via the roving tabindex pattern (active tab gets0, others get-1)
Screen Reader Behavior
Screen readers will recognize the native disclosure widgets and allow users to navigate between tabs using the roving tabindex pattern.
<!-- Always include an aria-label on tab-set --><tab-set aria-label="Product information"> ...</tab-set>
JavaScript API
The component manages its state internally but you can programmatically control tabs by manipulating the underlying <details> elements.
const tabs = document.querySelector('tab-set'); // Get all tab panelsconst panels = tabs.querySelectorAll('details'); // Programmatically open a tabpanels[1].open = true; // Opens the second tab // Check which tab is openconst openIndex = [...panels].findIndex(p => p.open);console.log('Open tab index:', openIndex); // Listen for changestabs.addEventListener('tab-set:change', (e) => { console.log('New tab:', e.detail.index);});
View Transitions
Add transition to enable animated tab switches using the View Transitions API. Three animation types are available:
| Value | Effect |
|---|---|
fade (default) | Crossfade between panels |
slide | Directional slide — forward when moving right, backward when moving left |
scale | Scale down old panel, scale up new panel |
Design
Slides forward to the next tab, backward to previous.
Build
Direction is computed automatically from tab index.
Ship
Works with both mouse clicks and keyboard navigation.
<tab-set transition="slide" aria-label="Feature tour"> <details name="tour" open> <summary>Design</summary> <div><p>Slides forward to the next tab, backward to previous.</p></div> </details> <details name="tour"> <summary>Build</summary> <div><p>Direction is computed automatically from tab index.</p></div> </details> <details name="tour"> <summary>Ship</summary> <div><p>Works with both mouse clicks and keyboard navigation.</p></div> </details></tab-set>
Styling
The tabs component can be styled using CSS. The component adds ARIA attributes that can be used for styling.
/* Style the tab list */tab-set { display: block;} /* Style individual tabs */tab-set summary { /* Tab styling */} /* Active tab */tab-set summary[aria-selected="true"] { border-color: var(--color-interactive);} /* Tab panels */tab-set details > div { padding: var(--size-m);}