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

  1. Add Home/End keys to tabs to jump to first/last tab.