Overview
Build every public page to WCAG 2.2 Level AA. WCAG 2.2 (W3C Recommendation, republished December 2024) is the current standard; WCAG 3.0 is still a Working Draft (10 September 2026) and does not deprecate WCAG 2, and there is no WCAG 2.3. Treat each criterion below as a pass or fail gate.
Run the WCAG 2.2 AA checklist before release
- Text contrast 4.5:1, large text 3:1 (SC 1.4.3); UI borders and focus rings 3:1 (SC 1.4.11).
- Every control operable by keyboard (2.1.1), with no traps outside modals (2.1.2).
- Skip link first (2.4.1); visible focus (2.4.7); focus not fully hidden by sticky content (2.4.11, new in 2.2).
- Meaningful
alt, oralt=""for decoration (1.1.1); programmatic labels (1.3.1, 3.3.2). - Targets at least 24x24 CSS px or spaced (2.5.8, new); a non-drag alternative for any drag (2.5.7, new).
- No forced memory test at login (3.3.8, new); no re-asking for data already given in the process (3.3.7, Level A, new); help in the same place across pages (3.2.6, Level A, new).
- Reduced motion honored (2.3.3, Level AAA but cheap).
- ARIA only when no native element fits.
SC 4.1.1 Parsing was removed in 2.2.
Meet contrast ratios
Large text means 18pt regular or 14pt bold and up. Disabled controls have no requirement. Verify with the DevTools picker or axe-core; do not eyeball it.
Make everything keyboard operable
Tab and Shift+Tab reach every control in DOM order; Enter and Space activate buttons; Enter follows links; arrow keys move inside composite widgets; Esc closes modals and menus and returns focus to the trigger. Native controls do this for free; custom widgets must implement it (html-aria-patterns).
Keep DOM order equal to visual order; CSS order, row-reverse, and absolute positioning change only the visual order. Avoid positive tabindex; use tabindex="0" only for custom widgets and tabindex="-1" for elements you focus from script.
Show focus and keep it unobscured
Style focus with :focus-visible so keyboard users see a ring and pointer clicks stay clean. Never ship outline: none without a replacement. Reserve space for sticky headers so focused elements scroll into view below them, and do not leave dismissible banners, cookie bars, or chat launchers covering the focused element.
:focus-visible { outline: 3px solid #005fcc; outline-offset: 2px; }
html { scroll-padding-top: 4rem; } /* sticky header height */Add a skip link
Make the first focusable element a link to <main>, moved off-screen until focused (not display: none, which removes it from the tab order).
<a class="skip-link" href="#main">Skip to content</a>
<main id="main">...</main>Write alt text by purpose
- Informative: say what the image conveys in context (“Bar chart: 40% growth in Q2”).
- Decorative:
alt="". - Functional (icon button): name the action (“Search”, not “magnifying glass”).
- Complex (diagram): a short label plus a longer description nearby or on a linked page.
Never omit alt; some screen readers announce the file name.
Use native elements and label every input
Use <button> for actions, <a href> for navigation, <label> for every input, and <fieldset> with <legend> for radio and checkbox groups. A placeholder is not a label. Binding patterns are in forms; ARIA rules are in html-aria-patterns.
Size targets and avoid drag-only interactions
SC 2.5.8 requires a 24x24 CSS px target, or enough spacing that a 24px circle centered on the target touches no other target. Inline text links are exempt; buttons, checkboxes, and icon controls are not. Give icon controls min-width: 24px; min-height: 24px. SC 2.5.7 requires every drag interaction (sortable lists, sliders) to have a single-pointer alternative such as buttons.
Do not block password managers
SC 3.3.8 forbids requiring users to transcribe or remember a credential unless an alternative exists. Allow paste and password-manager autofill in login and sign-up fields, with correct autocomplete tokens, and offer an alternative such as passkeys or an emailed link.
Respect reduced motion
SC 2.3.3 is Level AAA, but the cost is one media query. See css-animations for the stylesheet. For JavaScript animation, check matchMedia("(prefers-reduced-motion: reduce)").matches and skip or simplify the motion.
Automate, then test by hand
Automated tools catch only part of WCAG; they miss reading order, focus management, and meaning. Add axe-core (such as @axe-core/playwright) or pa11y to CI, then do one keyboard and one screen-reader pass per major change.
npx pa11y --standard WCAG2AA http://localhost:8080/See run-pa11y-locally for local setup and run-pa11y-locally for per-route budgets.