Tabs & Toolbars
Tabs
AvailableSwitches content within the same area. Uses arrow key navigation, aria-selected, and roving tabindex.
What Are Tabs?
Tabs are a UI pattern that layers multiple panels of content in the same area, letting users switch which panel is visible by selecting a tab label at the top. They are commonly used in settings screens, product details, and dashboards — anywhere you want to present related information while saving space.
Visually, tabs may look like a row of buttons, but assistive technologies need to know"this is a group of tabs and which one is currently selected."
Why Does Accessibility Matter?
- Keyboard-only users. If tabs are built with
<div>elements, they cannot receive focus via Tab and cannot be switched. Also, if the tab group is not treated as a single unit, users must press Tab once per tab, which is cumbersome. - Screen reader users. Without
role="tab"andaria-selected, the control is not recognized as a tab, and the current selection state is not conveyed — it just sounds like a row of plain text.
The two key techniques are "one tab stop for the entire tab group (roving tabindex)" and"conveying the selection state with aria-selected."
Live Demo (Recommended Implementation)
The tabs below follow the APG pattern. Try operating them without a mouse, using only the keyboard.
Edit your public profile, such as display name and avatar.
Change your password or set up two-factor authentication.
Choose how to receive email and push notifications.
Try it: Tab into the selected tab → use ← → to move between tabs (the panel switches simultaneously) → Home / End to jump to the first or last tab → Tab again to move into the panel.
Tip
When a screen reader focuses on a tab, it announces something like "Profile, tab, selected, 1 of 3" — conveying the role, selection state, and position. This is the effect ofrole="tab" and aria-selected.
Keyboard Interaction
| Key | Action | Priority |
|---|---|---|
| Tab | Enter / exit the tab list (the entire list is a single tab stop) | Required |
| ← / → | Move to the previous / next tab (with automatic activation, also selects the tab) | Required |
| Home / End | Move to the first / last tab | Optional |
| Enter / Space | Select the focused tab in manual activation mode | Optional |
Note
If the panel content is lightweight and can be displayed instantly, automatic activation— where pressing an arrow key both moves focus and selects the tab — provides the best experience (this demo uses this approach). If displaying the panel requires a network request or heavy processing, use manual activation, where Enter/Space confirms the selection.
Required WAI-ARIA Roles and Properties
| Target | Attribute / Role | Meaning |
|---|---|---|
| Tab list container | role="tablist" + aria-label | Conveys that the element is a collection of tabs and provides its accessible name. |
| Each tab | role="tab" | Identifies the element as a tab. Using <button> as the base element is recommended. |
| Each tab | aria-selected="true | false" | Indicates which tab is currently selected. Must be updated on every tab switch. |
| Each tab | aria-controls="<panel id>" | Points to the panel controlled by this tab. |
| Each tab | tabindex="0 | -1" | Set to 0 for the selected tab and -1 for all others (roving tabindex). |
| Each panel | role="tabpanel" + aria-labelledby="<tab id>" | A panel labeled by its corresponding tab. |
| Each panel | tabindex="0" | Ensures the panel itself is focusable via Tab even when it contains no focusable elements. |
| Hidden panels | hidden | Hides unselected panels from both screen readers and interaction. |
Implementation: Recommended Pattern (Good)
Good / Recommended
Tabs use <button role="tab">, and panels use role="tabpanel". Keep the selection state in sync via aria-selected and roving tabindex.
Markup:
<div class="tabs">
<div role="tablist" aria-label="Account settings">
<button type="button"
role="tab"
id="tabs-tab-profile"
aria-selected="true"
aria-controls="tabs-panel-profile"
tabindex="0">Profile</button>
<button type="button"
role="tab"
id="tabs-tab-security"
aria-selected="false"
aria-controls="tabs-panel-security"
tabindex="-1">Security</button>
</div>
<div role="tabpanel"
id="tabs-panel-profile"
aria-labelledby="tabs-tab-profile"
tabindex="0">Profile content…</div>
<div role="tabpanel"
id="tabs-panel-security"
aria-labelledby="tabs-tab-security"
tabindex="0"
hidden>Security content…</div>
</div>Switching script (syncs aria-selected, tabindex, and hidden in one place):
document.querySelectorAll('[data-tabs]').forEach((root) => {
const tabs = Array.from(root.querySelectorAll('[role="tab"]'));
function select(tab) {
tabs.forEach((t) => {
const selected = t === tab;
// Keep aria-selected and roving tabindex in sync
t.setAttribute('aria-selected', String(selected));
t.tabIndex = selected ? 0 : -1; // Only the selected tab is reachable via Tab
const panel = document.getElementById(t.getAttribute('aria-controls'));
if (panel) panel.hidden = !selected;
});
}
tabs.forEach((tab, i) => {
tab.addEventListener('click', () => select(tab));
tab.addEventListener('keydown', (e) => {
let next = null;
if (e.key === 'ArrowRight') next = (i + 1) % tabs.length;
else if (e.key === 'ArrowLeft') next = (i - 1 + tabs.length) % tabs.length;
else if (e.key === 'Home') next = 0;
else if (e.key === 'End') next = tabs.length - 1;
if (next !== null) {
e.preventDefault();
tabs[next].focus(); // Move focus
select(tabs[next]); // Select the tab at the same time (automatic activation)
}
});
});
});Anti-pattern (Bad)
The example below uses only <div> elements and onclick handlers.It switches with a mouse, but is completely inoperable via keyboard.
Edit your public profile, such as display name and avatar.
Change your password or set up two-factor authentication.
Choose how to receive email and push notifications.
Try it: pressing Tab does not focus any tab, and ← → does not switch between them. A screen reader cannot identify them as 'tabs' or determine which is 'selected.'
<!-- ❌ Anti-pattern -->
<div class="tabs">
<div class="tablist">
<!-- div + onclick. No role, no aria-selected, no keyboard support -->
<div class="tab" onclick="show('p1')">Profile</div>
<div class="tab" onclick="show('p2')">Security</div>
</div>
<div id="p1">Profile content…</div>
<div id="p2" style="display:none">Security content…</div>
</div>Bad / Avoid
Problems with this implementation:
- Not keyboard operable —
divelements cannot receive focus, and arrow keys have no effect. - Not recognized as tabs — without
role="tablist"/role="tab", they sound like plain text. - Selection state not conveyed — without
aria-selected, there is no way to tell which tab is active. - Panels not associated — without
aria-controls/aria-labelledby, the relationship between tabs and panels is lost.
Implementation Checklist
- Tabs are wrapped in a container with role="tablist" and aria-label
- Each tab is a <button role="tab">
- aria-selected always matches the selection state
- Only the selected tab has tabindex="0"; others have -1 (roving tabindex makes the tab list a single tab stop)
- Left/Right arrows move between tabs; Home/End move to the first/last tab
- Each tab has aria-controls; each panel has role="tabpanel", aria-labelledby, and tabindex="0"
- Hidden panels are excluded from screen readers and interaction via the hidden attribute
- Focus is visible (focus ring is not suppressed)
Source (English):Tabs Pattern — W3C APG(opens in a new tab)