Lesson 8 of 9 · 18m
Accessible Components by Default
Semantic HTML does the work — when you must build custom widgets, follow the ARIA pattern and keep keyboard+screen-reader paths real.
Most React accessibility problems are self-inflicted: rebuilding <button> as a <div>, modals that trap keyboard focus wrong, forms with no labels. The fix starts with boring technology — the platform already ships accessible primitives.
Use the element that does the job
// bad: a div that cosplays as a button
<div onClick={save} className="btn">Save</div>
// good: <button> gets click, Enter/Space, focus ring, and role="button" for free
<button onClick={save}>Save</button>Every div-as-widget means you now own keyboard handling, focus management, and ARIA semantics — work you almost certainly won't finish. <dialog> for modals, <details> for disclosures, <nav>/<main>/<aside> landmarks, real <label> for every input.
When you do build a widget, follow the pattern
The WAI-ARIA Authoring Practices specify behavior per widget: a dropdown menu is ArrowUp/Down + Enter + Escape, a tab list is ArrowLeft/Right + Home/End, role="tablist"/tab/tabpanel with aria-selected. React component libraries (Radix, Headless UI, React Aria) implement these correctly — use them; "I'll build my own combobox" is how you ship something unusable with a keyboard.
Focus management for dynamic UIs
- Route changes → move focus to the new page's
<h1>(give ittabIndex={-1}) or announce viaaria-live. - Modal opens → focus the first field; on close, return focus to the trigger.
- Toasts/alerts →
role="status"oraria-live="polite"so screen readers announce them. - Loading spinners →
aria-busyor visually-hidden text; a spinner alone says nothing.
State is visible to everyone or no one
Color alone isn't a state. aria-pressed on toggle buttons, aria-expanded on disclosures, aria-current="page" on nav links — and always a visible focus indicator (outline or box-shadow — outline: none without a replacement is a WCAG failure, not a style choice).
Field notes
- Test with the keyboard: unplug your mouse and use the app. It costs five minutes and finds most issues.
eslint-plugin-jsx-a11ycatches the mechanical stuff (missing alt, non-interactive click targets). Turn it on.- Reduced motion: respect
prefers-reduced-motionfor animations and autoplay.