Accessibility (WCAG 2.2)

Accessibility isn't a feature or a nice-to-have. It's a legal requirement in most jurisdictions, and it affects far more users than you think. About 15-20% of the population has some form of disability. Add in temporary impairments (broken arm, bright sunlight, loud environment) and situational limitations (holding a baby, slow connection), and you're designing for the majority.

The Four Principles (POUR)

All of WCAG is organized under four principles:

PrincipleMeaningKey Question
PerceivableUsers can perceive content through at least one senseCan all users see, hear, or otherwise access this content?
OperableUsers can interact with all controls and navigationCan all users navigate and use every interactive element?
UnderstandableUsers can understand content and how to use the interfaceIs the content readable and the interface predictable?
RobustWorks across technologies including assistive techDoes it work with screen readers, magnifiers, and future tools?

WCAG 2.2 Conformance Levels

LevelDescriptionLegal Context
AMinimum accessibility (25 criteria)Not sufficient for compliance in most frameworks
AAStandard compliance (13 additional criteria)Required by most laws (ADA, EAA, Section 508)
AAAEnhanced accessibility (23 additional criteria)Aspirational. Rarely required, not always possible for all content

Target AA. It's the legal standard and covers the vast majority of real-world accessibility needs.

Key Requirements: Detailed Checklist

Perceivable

RequirementHow to ImplementLevel
Images have alt text<img alt="Description of what the image shows">. Decorative images: alt=""A
Videos have captionsSynchronized captions for all pre-recorded video contentA
Videos have audio descriptionsNarrated descriptions of visual content for blind usersAA
Color is not the only indicatorPair color with icons, text, or patterns. "Fields in red have errors" fails; use icons too.A
Text contrast ≥ 4.5:1Body text on its background must meet 4.5:1 ratioAA
Large text contrast ≥ 3:1Text 18pt+ (or 14pt+ bold) can have lower contrastAA
UI component contrast ≥ 3:1Borders, icons, form controls against their backgroundAA
Text resizable to 200%Content remains functional when browser zoom is 200%AA
Content works in both orientationsDon't lock to portrait or landscape unless essentialAA
Spacing adjustableContent works if line height is 1.5×, paragraph spacing 2×, letter spacing 0.12em, word spacing 0.16emAA

Operable

RequirementHow to ImplementLevel
All functionality via keyboardEvery interactive element reachable and usable with Tab, Enter, Space, Arrow keysA
No keyboard trapsUsers can always Tab away from any element. Modal dialogs must trap focus but provide an escape.A
Skip linksFirst Tab stop: "Skip to main content" linkA
Focus order is logicalTab order follows visual reading order (top-to-bottom, left-to-right)A
Focus indicator visible2px+ outline visible on the currently focused element. Don't remove :focus styles.AA
No flashing contentNothing flashes more than 3 times per secondA
Motion can be pausedAuto-playing carousels, animations, and video must have pause controlsA
Touch targets ≥ 24×24pxMinimum target size for touch and clickAA
Touch targets ≥ 44×44pxRecommended target sizeAAA
Dragging has alternativesEvery drag-and-drop interaction must have a non-drag alternativeAA

Understandable

RequirementHow to ImplementLevel
Page language declared<html lang="en"> on every pageA
Language changes marked<span lang="fr">Bonjour</span> for inline foreign textAA
Navigation consistentSame navigation in the same order on all pagesAA
Labels consistentSame action called by the same name everywhereAA
Form inputs have visible labelsEvery input has a <label>, not just placeholder textA
Errors identified and describedSpecific error messages, not just "Invalid input"A
Error suggestions providedTell users how to fix the errorAA
Important actions reversibleAllow undo, or require confirmation for destructive actionsA
Help location consistentHelp/support link in the same position across pagesA
Redundant entry avoidedDon't ask for the same information twice in a processA

Robust

RequirementHow to ImplementLevel
Valid HTMLUse proper semantic elements, close tags, unique IDsA
ARIA used correctlyUse ARIA only when native HTML can't achieve the patternA
Name, role, value accessibleAll interactive elements expose their name, role, and state to assistive techA
Status messages announcedUse aria-live for dynamic content changes (toasts, counters, errors)AA

WCAG 2.2 New Criteria (2023)

These are the criteria added in WCAG 2.2 that you need to know:

CriterionLevelWhat It RequiresPractical Impact
Focus Not Obscured (Minimum)AAFocused element is at least partially visible (not completely hidden behind a sticky header or modal)Check that sticky headers don't cover focused elements when tabbing
Focus Not Obscured (Enhanced)AAAFocused element is fully visibleScroll elements into view on focus
Dragging MovementsAAAlternative to drag-and-drop for all interactionsProvide up/down buttons for reordering, click-to-place for positioning
Target Size (Minimum)AAInteractive targets are at least 24×24px, or have sufficient spacingAudit all small links, icons, and controls
Consistent HelpAHelp mechanism (chat, phone, FAQ link) appears in the same relative location on all pagesStandardize help link position
Redundant EntryADon't ask users to re-enter information already provided in the same processAuto-populate previously entered data
Accessible Authentication (Minimum)ADon't require cognitive function tests (memorizing a password, solving a CAPTCHA) unless alternatives existSupport password managers, provide alternatives to CAPTCHA
Accessible Authentication (Enhanced)AAANo cognitive tests even as secondary verificationUse biometrics, email links, or passkeys

ARIA: Practical Guide

The Rules of ARIA

  1. Don't use ARIA if native HTML works. A <button> is better than <div role="button"> in every way.
  2. Don't change native semantics unless you must. Don't add role="heading" to a <button>.
  3. All interactive ARIA controls must be keyboard-accessible. If you add role="button", you must also handle Enter and Space key events.
  4. Don't use role="presentation" or aria-hidden="true" on focusable elements. You'll make things invisible to screen readers but still keyboard-focusable.
  5. All interactive elements must have an accessible name. Via aria-label, aria-labelledby, or visible text content.

Common ARIA Patterns

<!-- Landmark regions -->
<header role="banner">
<nav aria-label="Main navigation">
<main role="main">
<aside aria-label="Related articles">
<footer role="contentinfo">

<!-- When you have multiple navs, label them -->
<nav aria-label="Main navigation">...</nav>
<nav aria-label="Footer navigation">...</nav>

<!-- Live regions for dynamic content -->
<div aria-live="polite">  <!-- Non-urgent updates -->
  Items in cart: 3
</div>
<div aria-live="assertive">  <!-- Urgent alerts -->
  Error: Payment failed
</div>

<!-- Expandable content -->
<button aria-expanded="false" aria-controls="panel-1">
  Show details
</button>
<div id="panel-1" hidden>
  Panel content here
</div>

<!-- Tabs -->
<div role="tablist" aria-label="Account settings">
  <button role="tab" aria-selected="true" aria-controls="panel-1">
    Profile
  </button>
  <button role="tab" aria-selected="false" aria-controls="panel-2">
    Security
  </button>
</div>
<div role="tabpanel" id="panel-1">Profile content</div>
<div role="tabpanel" id="panel-2" hidden>Security content</div>

<!-- Modal dialog -->
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
  <h2 id="dialog-title">Delete account?</h2>
  <p>This action cannot be undone.</p>
  <button>Cancel</button>
  <button>Delete</button>
</div>

<!-- Form errors -->
<label for="email">Email address</label>
<input id="email"
  type="email"
  aria-describedby="email-error"
  aria-invalid="true"
>
<span id="email-error" role="alert">
  Please enter a valid email address
</span>

<!-- Loading state -->
<button aria-busy="true" aria-disabled="true">
  <span class="spinner"></span>
  Saving...
</button>

Semantic HTML Cheat Sheet

Prefer these over ARIA when possible:

Instead ofUse
<div role="button"><button>
<div role="navigation"><nav>
<div role="main"><main>
<span role="link"><a href>
<div role="heading" aria-level="2"><h2>
<div role="list"> + <div role="listitem"><ul> + <li>
<div role="img" aria-label="..."><img alt="...">
<div role="checkbox"><input type="checkbox">

Keyboard Navigation

Expected Keyboard Behaviors

KeyExpected Action
TabMove to next focusable element
Shift+TabMove to previous focusable element
EnterActivate links, buttons, submit forms
SpaceActivate buttons, toggle checkboxes
EscapeClose modals, dropdowns, popovers
Arrow keysNavigate within widgets (tabs, menus, radio groups, sliders)
Home/EndJump to first/last item in a list or widget

Focus Management

ScenarioFocus Behavior
Modal opensFocus moves to the first focusable element inside the modal
Modal closesFocus returns to the element that triggered the modal
Content deletedFocus moves to the next logical element (next item in list, or parent)
New page section loadsFocus moves to the new content's heading or first element
Error on form submitFocus moves to the first field with an error

Focus Trap for Modals

When a modal is open, Tab should cycle within the modal only. It shouldn't escape to the page behind it:

// Focus trap: cycle Tab within modal
function trapFocus(modal) {
  const focusable = modal.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  modal.addEventListener('keydown', (e) => {
    if (e.key === 'Tab') {
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault();
        last.focus();
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
    if (e.key === 'Escape') {
      closeModal();
    }
  });
}

Testing for Accessibility

Automated Testing (Catches ~30-40% of Issues)

ToolTypeWhat It Catches
axe DevToolsBrowser extensionMissing alt text, contrast failures, ARIA errors
WAVEBrowser extensionVisual overlay of accessibility issues
LighthouseChrome DevToolsAccessibility score + specific failures
Pa11yCLI / CI integrationAutomated testing in pipelines
eslint-plugin-jsx-a11yLinter pluginCatches issues during development

Manual Testing Checklist

Do this for every major feature:

TestHowWhat You're Checking
Keyboard onlyUnplug mouse, Tab through entire pageAll functionality reachable, logical order, no traps
Screen readerUse VoiceOver (Mac), NVDA (Windows), TalkBack (Android)All content announced, forms labeled, states communicated
Zoom to 200%Ctrl/Cmd + zoom browser to 200%Content still readable, no horizontal scroll, nothing overlaps
Color blindnessChrome DevTools → Rendering → Emulate vision deficienciesInformation not lost without color
Reduced motionEnable reduced motion in OS settingsAnimations reduced/eliminated, no motion sickness triggers
High contrastEnable Windows High Contrast ModeContent still visible, borders present, focus visible

Screen Reader Testing Basics

Test these specific things:

CheckExpected Behavior
Navigate by headings (H key)Headings form a logical outline (H1 → H2 → H3, no skipping levels)
Navigate by landmarksMain, nav, footer, and aside regions are announced
Form fieldsLabel announced when focusing each field
Buttons and linksText content or aria-label announced
ImagesAlt text announced (or properly hidden if decorative)
Dynamic contentToast notifications, error messages, and updates announced
TablesHeaders associated with data cells

Common Mistakes

MistakeImpactFix
Removing :focus outlinesKeyboard users can't see where they areKeep visible focus indicators. Style them, don't remove them.
Missing alt textScreen reader users don't know what images showAdd descriptive alt text. Decorative images get alt="".
Color-only indicatorsColorblind users miss status informationAdd icons, text, or patterns alongside color.
Div soup with ARIA overloadFragile, often broken for screen readersUse semantic HTML first. Only add ARIA when native HTML can't do it.
Modals without focus trapKeyboard users Tab behind the modal into invisible contentTrap focus inside modal, return focus on close.
Auto-playing mediaDisruptive, especially for screen reader usersNever autoplay with sound. Provide pause controls.
Missing form labelsScreen readers announce "edit text" with no contextEvery input needs a <label for="id">.
Heading level skippingScreen reader navigation by headings becomes confusingUse H1 → H2 → H3 in order. Don't skip from H1 to H4.
Non-interactive elements made focusableConfusing Tab orderDon't add tabindex="0" to divs and spans unless they're truly interactive.

Accessibility Testing in CI/CD

# Run axe-core tests in your CI pipeline
npx @axe-core/cli https://your-site.com

# Use Pa11y for automated testing
npx pa11y https://your-site.com --standard WCAG2AA

# Lighthouse CI
npx lhci autorun --collect.url=https://your-site.com

Set a threshold: Fail the build if the accessibility score drops below a defined level (e.g., 90/100 on Lighthouse).

Key Takeaways

  • Target WCAG 2.2 AA. It's the legal standard and covers real-world needs.
  • Use semantic HTML first. Only reach for ARIA when native elements can't achieve the pattern.
  • Test with keyboard, screen reader, zoom, and color blindness simulation. Automated tools catch less than half of issues.
  • Color must never be the only indicator. Always pair with icons, text, or patterns.
  • Every interactive element needs visible focus indicators, keyboard access, and an accessible name.
  • Manage focus explicitly: move focus into modals, back on close, to errors on submit.
  • Alt text: describe what the image shows, not what it is. "Team celebrating product launch" not "photo.jpg".
  • Accessibility benefits everyone: screen readers, keyboard users, slow connections, bright sunlight, one-handed use.