Take the basic tabs and give them the ARIA tabs contract so assistive technology understands the pattern: a tablist containing tabs, each linked to the tabpanel it controls, with the selected tab marked. Visually unchanged — but now a screen reader announces "tab, 1 of 3, selected" and knows which panel a tab reveals.
// A self-contained component. No props.
function App(): JSX.Element;
The same tabs, now with role="tablist", role="tab", role="tabpanel", aria-selected, and the aria-controls/aria-labelledby links.
<div role="tablist" aria-label="Product information">
<button role="tab" id="tab-pricing"
aria-selected="true" aria-controls="panel-pricing">Pricing</button>
…
</div>
<div role="tabpanel" id="panel-pricing" aria-labelledby="tab-pricing" tabindex="0">…</div>
aria-selected="true" on the active tab; aria-controls links tab → panel;
aria-labelledby links panel → tab.
role="tablist" wraps the tabs; each tab button is role="tab"; the content area is role="tabpanel".aria-selected="true" on the active tab and false on the others (a class isn't enough for AT).aria-controls is the panel's id; the panel's aria-labelledby is the active tab's id (so the panel is named by its tab).tabIndex={0} so keyboard users can move into its content.The full solution is part of Premium
Walkthrough, edge cases, complexity notes, and the runnable editor unlock with a Premium subscription.
Submissions are part of Premium
Unlock community code, comments, reactions, and framework-specific approaches.
Take the basic tabs and give them the ARIA tabs contract so assistive technology understands the pattern: a tablist containing tabs, each linked to the tabpanel it controls, with the selected tab marked. Visually unchanged — but now a screen reader announces "tab, 1 of 3, selected" and knows which panel a tab reveals.
// A self-contained component. No props.
function App(): JSX.Element;
The same tabs, now with role="tablist", role="tab", role="tabpanel", aria-selected, and the aria-controls/aria-labelledby links.
<div role="tablist" aria-label="Product information">
<button role="tab" id="tab-pricing"
aria-selected="true" aria-controls="panel-pricing">Pricing</button>
…
</div>
<div role="tabpanel" id="panel-pricing" aria-labelledby="tab-pricing" tabindex="0">…</div>
aria-selected="true" on the active tab; aria-controls links tab → panel;
aria-labelledby links panel → tab.
role="tablist" wraps the tabs; each tab button is role="tab"; the content area is role="tabpanel".aria-selected="true" on the active tab and false on the others (a class isn't enough for AT).aria-controls is the panel's id; the panel's aria-labelledby is the active tab's id (so the panel is named by its tab).tabIndex={0} so keyboard users can move into its content.The full solution is part of Premium
Walkthrough, edge cases, complexity notes, and the runnable editor unlock with a Premium subscription.
Submissions are part of Premium
Unlock community code, comments, reactions, and framework-specific approaches.
Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.