Structure
tablist contains tabs. Each tab controls a tabpanel via aria-controls.
Loading ...
tablistA collection of tabs that each control the visibility of a corresponding tabpanel. Tablists organize related content into digestible sections.
Tabs with roving tabindex, arrow-key navigation, and ARIA-linked tabpanels.
tablist contains tabs. Each tab controls a tabpanel via aria-controls.
Only one tabpanel should be visible at a time (unless multi-select tabs).
Announces βtab 2 of Nβ. Users expect arrow keys to change tabs.
Switch between charts inside a compact card.
Organize profile, billing, and security settings.
Show code samples in different languages or frameworks.
role="tab" items, each with aria-controls referencing its tabpanel. Manage aria-selected + tabindex.
role="tabpanel" with id matching aria-controls. Hidden panels stay in DOM but are aria-hidden when inactive.
Declare aria-orientation when tabs are vertical to help assistive tech.
| Action | Keys | Result |
|---|---|---|
| Move between tabs | ArrowLeft / ArrowRight (horizontal) or ArrowUp / ArrowDown (vertical) | Focuses and optionally activates the adjacent tab. |
| Jump to start/end | Home / End | Moves focus to the first or last tab in the list. |
| Activate tab | Space / Enter | Displays the associated tabpanel when activation follows focus. |
| Ctrl + Page Up/Page Down | Ctrl + PageUp / Ctrl + PageDown | Switches tabs in many desktop apps; mimic when building complex tooling. |
aria-controlsRequiredConnects a tab to its tabpanel by id.
aria-selectedRequiredSet true on the active tab and false on the rest.
aria-expandedOptionalOptional when tabs double as accordions; indicates whether the panel is open.
aria-orientationOptionalSet to vertical when tabs are stacked.
aria-labelledbyOptionalEach tabpanel should reference the id of its owning tab.
Use roving tabindex so only the active tab is focusable (tabindex="0"). Others should be -1.
If activating on focus causes performance concerns, allow manual activation via Enter/Space.
Provide clear focus and selection indicators for both keyboard and pointer interactions.
Maintain equal heights to avoid layout shifts when switching between tab contents.
If panels contain forms, manage focus so submitting inside a panel does not jump back to the tabs.
Tabs manage aria-selected and control matching panels.
<div role="tablist" aria-label="Billing tabs">
<button role="tab" id="tab-overview" aria-controls="panel-overview" aria-selected="true" tabindex="0">
Overview
</button>
<button role="tab" id="tab-invoices" aria-controls="panel-invoices" aria-selected="false" tabindex="-1">
Invoices
</button>
<button role="tab" id="tab-settings" aria-controls="panel-settings" aria-selected="false" tabindex="-1">
Settings
</button>
</div>
<section id="panel-overview" role="tabpanel" aria-labelledby="tab-overview">
<!-- content -->
</section>
<section id="panel-invoices" role="tabpanel" aria-labelledby="tab-invoices" hidden>
<!-- content -->
</section>
<section id="panel-settings" role="tabpanel" aria-labelledby="tab-settings" hidden>
<!-- content -->
</section>State-driven tablist with keyboard management.
import { useState } from 'react'
const tabs = [
{ id: 'overview', label: 'Overview' },
{ id: 'activity', label: 'Activity' },
{ id: 'logs', label: 'Logs' },
]
export function TabExample() {
const [active, setActive] = useState('overview')
return (
<>
<div role="tablist" aria-label="Project sections">
{tabs.map((tab) => (
<button
key={tab.id}
role="tab"
id={`tab-${tab.id}`}
aria-controls={`panel-${tab.id}`}
aria-selected={active === tab.id}
tabIndex={active === tab.id ? 0 : -1}
onClick={() => setActive(tab.id)}
>
{tab.label}
</button>
))}
</div>
{tabs.map((tab) => (
<section
key={tab.id}
id={`panel-${tab.id}`}
role="tabpanel"
aria-labelledby={`tab-${tab.id}`}
hidden={active !== tab.id}
>
Panel: {tab.label}
</section>
))}
</>
)
}Prevent layout jumps by reserving space for the tallest panel.
Use underline/indicator to show the active tab for sighted users.
Let users know when new content loads via aria-live if asynchronous.
Completely removing the panel from the accessibility tree prevents screen readers from finding content.
Use hidden attribute or aria-hidden + CSS visibility while keeping the DOM node available.
Users must tab through all tabs before reaching content.
Only the active tab should be tabbable; use arrow keys for internal navigation.
Interactive element inside the tablist.
Learn moreContainer for content associated with a tab.
Learn moreAttribute used to announce vertical tablists.
Learn more