Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
Tabbed content panels with keyboard navigation and ARIA semantics.
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>
| Attribute | Values | Default | Description |
|---|---|---|---|
aria-label | string | — | Accessible label for the tab group |
transition | "fade", "slide", "scale" | — | View Transition animation between tab panels |
| Element | Required | Description |
|---|---|---|
<details> | yes | One per tab — native disclosure element provides open/close state |
<summary> | yes | Tab label inside each <details> |
| 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. |
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.
When JavaScript is disabled or fails to load, the tabs degrade gracefully to an accordion-style interface where each panel can be expanded independently.
<details>/<summary> semantics are preserved (no role overrides)aria-controls and aria-labelledby link each tab to its panelaria-selected state is managed on each summarytabindex is managed for roving focusWhen 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:
Press ArrowRight to move to the next tab.
Press ArrowLeft to move to the previous tab, or End to jump to the last.
Press Home to jump back to the first tab.
Tabs can contain any HTML content, including forms, images, and other components.
Tabs can contain rich text content with proper typography.
Including blockquotes, lists, and other typographic elements.
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>
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-controls on each <summary> points to its panelaria-labelledby on each panel points back to its summaryaria-selected reflects which tab is currently activetabindex is managed via the roving tabindex pattern (active tab gets 0, others get -1)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>
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);});
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 |
Slides forward to the next tab, backward to previous.
Direction is computed automatically from tab index.
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>
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);}