Skip to content

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 it tabIndex={-1}) or announce via aria-live.
  • Modal opens → focus the first field; on close, return focus to the trigger.
  • Toasts/alerts → role="status" or aria-live="polite" so screen readers announce them.
  • Loading spinners → aria-busy or 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-a11y catches the mechanical stuff (missing alt, non-interactive click targets). Turn it on.
  • Reduced motion: respect prefers-reduced-motion for animations and autoplay.