React - Tabs & Accordion
Overview
Estimated time: 20–30 minutes
Build tabs and accordion components using ARIA roles and keyboard interactions.
Try it: Tabs
View source
function Tabs(){
const labels = ['One','Two','Three'];
const [idx, setIdx] = React.useState(0);
const ids = labels.map((_,i)=>({tab:`tab-${i}`, panel:`panel-${i}`}));
function onKey(e){
if (e.key==='ArrowRight') setIdx((idx+1)%labels.length);
if (e.key==='ArrowLeft') setIdx((idx-1+labels.length)%labels.length);
}
return (
<div>
<div role="tablist" aria-label="Sample tabs" onKeyDown={onKey}>
{labels.map((l,i)=>(
<button key={l} id={ids[i].tab} role="tab" aria-selected={i===idx} aria-controls={ids[i].panel} tabIndex={i===idx?0:-1} onClick={()=>setIdx(i)}>{l}</button>
))}
</div>
{labels.map((l,i)=>(
<div key={i} role="tabpanel" id={ids[i].panel} aria-labelledby={ids[i].tab} hidden={i!==idx}>Panel {l}</div>
))}
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-tabs')).render(<Tabs />);
Try it: Accordion
View source
function Accordion(){
const [open, setOpen] = React.useState(0);
const items = ['A','B','C'];
return (
<div>
{items.map((label,i)=>(
<div key={i}>
<h4><button aria-expanded={open===i} aria-controls={`sect-${i}`} id={`btn-${i}`} onClick={()=> setOpen(o => o===i? -1 : i)}>Section {label}</button></h4>
<section id={`sect-${i}`} role="region" aria-labelledby={`btn-${i}`} hidden={open!==i}>
<p>Content {label}</p>
</section>
</div>
))}
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-accordion')).render(<Accordion />);
Syntax primer
- Tabs: use role="tablist"/"tab"/"tabpanel" and manage focus with arrow keys.
- Accordion: toggle aria-expanded and hide/show controlled content.
Common pitfalls
- Not syncing aria-selected/aria-expanded with visual state.
Exercises
- Add Home/End keys to tabs to jump to first/last tab.