Skip to content
fearchitect
Quality, Observability & Cross-Cutting

Accessibility (a11y)

WCAG 2.2 AA: semantic HTML, keyboard nav, and ARIA done right.

By Abas TurabliReviewed

Summary

Accessibility means every user — regardless of motor, visual, or cognitive ability — can perceive, operate, and understand your UI. WCAG 2.2 AA is the legal bar: four principles (POUR) cover 50 success criteria. Start with semantic HTML; reach for ARIA only when native elements cannot express the state. Axe catches ~57% automatically; the rest need manual screen-reader testing.

Jump to the interview angle

Accessibility (a11y)

A UI is accessible when every user — including those who use a keyboard only, a screen reader, or switch access — can perceive, operate, and understand it. WCAG 2.2 AA defines the bar: 50 success criteria grouped under four principles (POUR). Passing AA is required by law in many jurisdictions (ADA, EN 301 549) and improves usability for all users. The concrete starting point is correct semantic HTML; ARIA fills in what HTML cannot express.

The POUR principles at a glance

  • Perceivable: content reaches at least one sense — text alternatives, captions, and contrast ≥4.5:1 at AA.
  • Operable: every interaction works via keyboard alone; no content flashes more than 3 Hz.
  • Understandable: language is declared, errors are described, consistent navigation across pages.
  • R (4th principle): valid markup and correct ARIA roles/states so assistive tech can parse the tree.

Accessible disclosure button with focus management

A disclosure widget (show/hide) is the canonical ARIA pattern: a <button> controls a region via aria-expanded and aria-controls. No ARIA role needed — <button> is already role="button" with keyboard support built in.

Disclosure button — React + TypeScripttsx
import { useId, useState } from "react";

export function Disclosure({
  label,
  children,
}: {
  label: string;
  children: React.ReactNode;
}) {
  const panelId = useId();
  const [open, setOpen] = useState(false);

  return (
    <div>
      <button
        type="button"
        aria-expanded={open}
        aria-controls={panelId}
        onClick={() => setOpen((v) => !v)}
      >
        {label}
      </button>
      <div id={panelId} hidden={!open}>
        {children}
      </div>
    </div>
  );
}

aria-expanded reflects open/closed state; aria-controls links the button to its panel. hidden removes the panel from the accessibility tree when closed.

How to implement a focus trap in a modal

  1. 1

    Move focus into the modal on open

    When the modal mounts, call firstFocusableElement.focus(). Store the element that triggered the open so you can restore focus on close. Use useEffect with the modal's open state as the dependency.

  2. 2

    Intercept Tab and Shift-Tab

    On keydown, collect all focusable elements inside the modal (button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])). If Tab is pressed on the last one, wrap to the first; Shift-Tab on the first wraps to the last.

  3. 3

    Intercept Escape

    Close the modal and return focus to the trigger element. Required by WCAG SC 2.1.2 (No Keyboard Trap) — users must always have a keyboard path to exit.

  4. 4

    Mark the backdrop inert

    Set inert on everything outside the modal (document.body children except the modal container). The inert attribute removes elements from the tab order and accessibility tree without display:none.

No ARIA is better than bad ARIA

Adding an incorrect role or mismatched aria-* state actively breaks screen readers — they announce wrong semantics with no visible signal to sighted users. If you add role="button" to a <div>, you must also add tabindex="0" and keyboard handlers for Enter and Space. A native <button> gives you all of this for free. Reach for ARIA only when native elements cannot express the required pattern.

Testing checklist

  • Automated (axe-core via Playwright or jest-axe): catches color contrast, missing labels, duplicate IDs — ~57% of issues.
  • Keyboard-only: tab through every flow, confirm visible focus indicator, skip links work, modals trap focus.
  • Screen reader: test with VoiceOver (macOS/iOS) and NVDA+Chrome (Windows) — the two most-used combinations per WebAIM survey.
  • Zoom to 200%: text reflows, no horizontal scroll, interactive targets remain ≥44×44 px (WCAG 2.5.5).

Interview angle

Interviewers probe whether you know the semantic-first hierarchy, when ARIA is required vs. harmful, and how to manage focus in SPAs.

Soundbite: "Semantic HTML is free accessibility — a <button> gives you keyboard events, focus, and role with zero ARIA. Add ARIA only when the native element can't express the state."

Key terms

WCAG 2.2 AA
W3C standard with 50 success criteria at AA level — the legal minimum in most jurisdictions.
POUR
The four WCAG principles: Perceivable, Operable, Understandable, Robust.
ARIA
Accessible Rich Internet Applications — attributes that expose role, state, and properties to assistive tech.
accessible name
The text a screen reader announces for an element; computed from label, aria-label, or aria-labelledby.
focus trap
Constraining Tab/Shift-Tab to within an open modal so keyboard users cannot reach content behind it.

Further reading

Search fearchitect

Jump to a topic, mode, or action.