Frontend / Accessibility / 06_accessible_components.md

Accessible Components (APG Patterns)

Updated 5 interview angles 4 min read source
On this page10
  1. What does an accessible modal dialog require?
  2. Tabs — markup and keyboard?
  3. Disclosure (show/hide) — the simplest pattern?
  4. Why is a combobox/autocomplete the hardest widget?
  5. Menu button vs a list of links — what’s the difference?
  6. When do you build vs adopt a library for these?
  7. Gotchas
  8. Related
  9. Further reading
  10. Interview angle

Accessible Components (APG Patterns)

The WAI-ARIA Authoring Practices Guide (APG) specifies the role/state markup and the keyboard interaction for common widgets — dialog, disclosure, tabs, menu, combobox. The senior takeaway: these are genuinely hard to get right (focus, keyboard, ARIA state, screen-reader announcement all at once), so use an accessible primitive library (Radix, React Aria, Headless UI) rather than hand-rolling — but know the patterns so you can verify them. See Headless UI and Radix.

What does an accessible modal dialog require?

Markup + behavior:

  • role="dialog" (or native <dialog>), aria-modal="true", named via aria-labelledby (its title).
  • On open: move focus into the dialog; trap focus inside it; mark the rest of the page inert/aria-hidden.
  • Escape closes it; on close restore focus to the trigger (Keyboard and Focus Management).

The native <dialog> element with .showModal() handles trap + Escape + top-layer + backdrop for you — prefer it or a vetted library.

Tabs — markup and keyboard?

html
<div role="tablist" aria-label="Account">
  <button role="tab" aria-selected="true"  aria-controls="p1" id="t1">Profile</button>
  <button role="tab" aria-selected="false" aria-controls="p2" id="t2" tabindex="-1">Billing</button>
</div>
<div role="tabpanel" id="p1" aria-labelledby="t1">…</div>
<div role="tabpanel" id="p2" aria-labelledby="t2" hidden>…</div>

Keyboard (roving tabindex — one Tab stop for the list):

Key Action
Tab move into the tablist (active tab), then to the panel
Left/Right (or Up/Down) move between tabs
Home/End first/last tab
Enter/Space activate (if manual activation)

aria-selected tracks the active tab; the active tab has tabindex="0", the rest -1.

Disclosure (show/hide) — the simplest pattern?

A button toggling a region:

html
<button aria-expanded="false" aria-controls="more">Details</button>
<div id="more" hidden>…</div>

Toggle aria-expanded and the hidden/visibility together. Native <details>/<summary> gives this for free — use it unless you need custom animation/behavior.

Why is a combobox/autocomplete the hardest widget?

It combines a text input, a popup listbox, async results, and keyboard navigation while focus stays in the input:

  • input: role="combobox", aria-expanded, aria-controls="listbox-id", aria-activedescendant="option-id".
  • popup: role="listbox" with role="option" children; the active option is referenced by aria-activedescendant (focus never leaves the input).
  • keyboard: Down/Up move the active option, Enter selects, Escape closes, typing filters.
  • results announced via the listbox; loading/empty states need a live region.

There are many edge cases (selection models, async, mobile). This is the canonical “don’t hand-roll — use Radix/React Aria/Downshift” widget.

A role="menu" (with role="menuitem") is for application menus (actions), navigated with arrow keys and a single Tab stop, opened from a aria-haspopup="menu" button. A site navigation list is just <nav><ul><li><a> — do not slap role="menu" on nav links; that changes the interaction model and confuses users. Most “dropdown menus” in nav are disclosures of links, not ARIA menus.

When do you build vs adopt a library for these?

Adopt. Radix Primitives, React Aria (Adobe), Headless UI, and Downshift implement the APG patterns with the focus/keyboard/ARIA correctness that takes months to get right and regresses easily. Build only a trivial disclosure/toggle, and even then prefer native (<details>, <dialog>). Know the patterns to audit the library and your usage. See Headless UI and Radix.

Gotchas

  • role="menu" on navigation — wrong model; reserve menus for action menus, use links/disclosure for nav.
  • hidden vs CSS hiding for tab panels — use hidden/display:none so hidden panels leave the tab order and AT tree.
  • Tooltips on :hover only — must also show on focus and be dismissible (Escape), and not contain interactive content (WCAG 1.4.13).
  • Animated/transitioned dialogs — don’t move focus before the element is actually in the DOM and focusable; respect prefers-reduced-motion.
  • Toasts — auto-dismiss can outrun a screen reader; give enough time or a persistent log; announce via a polite live region.
  • aria-activedescendant vs roving tabindex — combobox uses activedescendant (focus stays in input); tabs/menus use roving tabindex (focus moves to the item). Mixing them up breaks the widget.

Further reading

Interview angle 5

  • “Would you build a combobox from scratch?” - no. These widgets need role, state, keyboard interaction, focus management and announcement to all be correct simultaneously, and the specification for each is pages long. Use Radix, React Aria or Headless UI and verify against the ARIA Authoring Practices Guide.
  • “What does a correct dialog require?” - aria-modal and an accessible name, focus trapped inside, the background marked inert, Escape to close, and focus restored to the trigger afterwards. The native <dialog> element with showModal() gives you most of that for free, which is the reason to reach for it first.
  • “Which keyboard pattern for which widget?” - roving tabindex for tabs and menus, where focus genuinely moves between items; aria-activedescendant for a combobox, where DOM focus must stay in the input while the visual selection moves through the list.
  • “When is role="menu" wrong?” - on a navigation bar. menu implies an application-style action menu with arrow-key navigation and specific expectations. A list of links is a <nav> with a list, and mislabelling it makes navigation harder rather than more semantic.
  • “What is the general principle?” - native element first: <details>, <dialog>, <select>, <input type="date">. Every custom rebuild is behaviour you now own on every browser and assistive technology combination.