DEV Community

Gaurav Guha
Gaurav Guha

Posted on Originally published at thefrontkit.com

WCAG 2.1 AA Checklist for React and Next.js Applications

The problem

You need to ship a web app that meets WCAG 2.1 AA: the baseline most legal requirements and procurement processes reference: but the spec is dense and hard to map to actual component code. This checklist translates each criterion into patterns you can use during development, code review, and QA.

Principle 1: Perceivable

1.1 Text alternatives

1.1.1 Non-text content: Every non-text element needs a text alternative serving the same purpose.

// Good: Meaningful alt text
<img src="/dashboard-chart.png" alt="Monthly revenue chart showing 23% growth from January to March 2026" />

// Good: Decorative image
<img src="/decorative-wave.svg" alt="" />

// Good: Icon button with label
<button aria-label="Close dialog">
  <XIcon aria-hidden="true" />
</button>
Enter fullscreen mode Exit fullscreen mode

Key points:

  • All <img> elements have meaningful alt describing purpose, not appearance
  • Decorative images use alt="" so screen readers skip them
  • Icon buttons need aria-label or visually hidden text
  • SVG icons used as content need role="img" and aria-label
  • Charts and data visualizations need text descriptions or accessible data tables

1.3 Adaptable

1.3.1 Info and relationships: Structure conveyed visually must be conveyed programmatically.

// Good: Proper table structure
<table>
  <thead>
    <tr>
      <th scope="col">Name</th>
      <th scope="col">Role</th>
      <th scope="col">Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Jane Smith</td>
      <td>Admin</td>
      <td>Active</td>
    </tr>
  </tbody>
</table>
Enter fullscreen mode Exit fullscreen mode

Requirements:

  • Headings use proper <h1> through <h6> in logical order (no skipping levels)
  • Lists use <ul>, <ol>, or <dl>: not styled <div>s
  • Data tables use <table>, <thead>, <th>, <td> with scope attributes
  • Form inputs associate with labels via <label htmlFor="id"> or aria-labelledby
  • Related fields grouped with <fieldset> and <legend>
  • Landmarks: <nav>, <main>, <aside>, <header>, <footer>

1.3.5 Identify input purpose: Form inputs for personal data use appropriate autocomplete attributes.

<input type="email" name="email" autoComplete="email" />
<input type="text" name="name" autoComplete="name" />
<input type="tel" name="phone" autoComplete="tel" />
Enter fullscreen mode Exit fullscreen mode

1.4 Distinguishable

1.4.3 Contrast (minimum): The criterion most products fail.

  • Normal text (< 18pt / 24px): 4.5:1 contrast ratio
  • Large text (18pt+ bold or 24px+ regular): 3:1
  • Applies to both light and dark mode
  • Placeholder text must meet contrast requirements

Tools: Chrome DevTools (Inspect > computed styles > contrast ratio), axe browser extension, Stark plugin for Figma.

1.4.4 Resize text: Text resizes to 200% without loss of content or functionality.

  • Use relative units (rem, em, %) instead of px for font sizes
  • Layouts must not break at 200% browser zoom

1.4.10 Reflow: Content reflows at 320px viewport width (400% zoom on 1280px screen) without horizontal scrolling.

1.4.11 Non-text contrast: UI components (buttons, inputs, checkboxes, toggles) need 3:1 contrast against adjacent colors. Focus indicators, chart lines, and meaningful icons also need 3:1.

1.4.13 Content on hover or focus: Tooltips and popovers:

  • Dismissible with Escape key without moving pointer
  • Hoverable without disappearing
  • Stay visible until dismissed, focus moves, or trigger unhovered

Principle 2: Operable

2.1 Keyboard accessible

2.1.1 Keyboard: Every interactive element reachable and operable via keyboard (Tab, Enter, Space, Arrow keys, Escape). Custom components (dropdowns, modals, accordions, tabs) need appropriate keyboard handlers.

2.1.2 No keyboard trap: Focus can always move away using standard navigation. Modals trap focus when open but release on close.

// Good: Modal with focus trap that releases on close
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
  {/* Focus is trapped here while open */}
  <DialogPanel>
    <h2>Settings</h2>
    <button onClick={() => setIsOpen(false)}>Close</button>
  </DialogPanel>
</Dialog>
Enter fullscreen mode Exit fullscreen mode

2.2 Enough time

2.2.2 Pause, stop, hide: Moving, blinking, or auto-updating content can be paused, stopped, or hidden. Animations respect prefers-reduced-motion.

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
Enter fullscreen mode Exit fullscreen mode

2.4 Navigable

2.4.1 Bypass blocks: "Skip to main content" link as first focusable element. Landmark regions allow screen reader users to jump between sections.

// Skip link as first element in layout
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-white focus:text-black focus:rounded">
  Skip to main content
</a>
Enter fullscreen mode Exit fullscreen mode

2.4.3 Focus order: Tab order follows logical sequence matching visual layout. Dynamically added content (modals, toasts, expanded sections) receives focus appropriately. No tabindex values greater than 0.

2.4.7 Focus visible: All interactive elements have visible focus indicator when keyboard-navigated.

/* Good: Clear, visible focus indicator */
:focus-visible {
  outline: 2px solid #685FD4;
  outline-offset: 2px;
}
Enter fullscreen mode Exit fullscreen mode

2.5 Input modalities

2.5.3 Label in name: Visible text label of a component is included in its accessible name. A button saying "Submit" has an accessible name containing "Submit" (not "btn-action-01").

Principle 3: Understandable

3.1 Readable

3.1.1 Language of page: <html> element has correct lang attribute.

<html lang="en">
Enter fullscreen mode Exit fullscreen mode

3.1.2 Language of parts: Content in different language wrapped with appropriate lang attribute.

3.2 Predictable

3.2.3 Consistent navigation: Navigation menus appear in same location and order across pages.

3.2.4 Consistent identification: Components with same function have same label across the app. "Save" button is always "Save," not sometimes "Submit" or "Confirm."

3.3 Input assistance

3.3.1 Error identification: Form errors identified in text, not just color. Messages specify which field and what's wrong.

3.3.3 Error suggestion: When error detected and suggestions available, provide them. "Invalid email" suggests "Enter an email address like name@example.com."

3.3.4 Error prevention (legal, financial, data): Submissions involving legal, financial, or data changes are reversible, verified, or confirmed.

// Good: Confirmation before destructive action
<Dialog>
  <DialogTitle>Delete account?</DialogTitle>
  <DialogDescription>
    This will permanently delete your account and all associated data. This action cannot be undone.
  </DialogDescription>
  <button onClick={onCancel}>Cancel</button>
  <button onClick={onConfirm}>Delete account</button>
</Dialog>
Enter fullscreen mode Exit fullscreen mode

Principle 4: Robust

4.1 Compatible

4.1.2 Name, role, value: All interactive components have accessible name, role, and state. Custom components use appropriate ARIA roles. State changes communicated via aria-expanded, aria-selected, aria-checked, aria-disabled.

// Good: Custom accordion with proper ARIA
<div role="region" aria-labelledby="section-heading">
  <button
    aria-expanded={isOpen}
    aria-controls="section-content"
    id="section-heading"
  >
    Account Settings
  </button>
  <div id="section-content" role="region" hidden={!isOpen}>
    {/* Settings content */}
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

4.1.3 Status messages: Status messages announced to screen readers without receiving focus. Use role="status", role="alert", or aria-live="polite".

// Good: Toast notification announced to screen readers
<div role="status" aria-live="polite">
  {savedSuccessfully && "Settings saved successfully."}
</div>

// Good: Alert for errors
<div role="alert">
  {error && `Error: ${error.message}`}
</div>
Enter fullscreen mode Exit fullscreen mode

Testing checklist

Automated testing catches only ~30% of issues. Run this minimum routine:

Automated

  • Run axe DevTools or Lighthouse accessibility audit on every page
  • Add eslint-plugin-jsx-a11y to ESLint config for build-time checks
  • Include accessibility checks in CI pipeline

Manual keyboard

  • Navigate entire app using only Tab, Shift+Tab, Enter, Space, Arrow keys, Escape
  • Complete every user flow without a mouse
  • Verify focus indicators always visible during keyboard navigation

Screen reader

  • Test with VoiceOver (macOS) or NVDA (Windows) on main user flows
  • Verify headings, landmarks, and form labels announced correctly
  • Confirm dynamic content changes (loading states, errors, new items) announced

Visual

  • Test at 200% browser zoom
  • Test at 320px viewport width
  • Test with prefers-reduced-motion: reduce enabled
  • Test in both light and dark mode
  • Use contrast checker on all text and interactive elements

Component libraries that handle accessibility

Building every pattern from scratch is error-prone. These libraries provide accessible primitives:

  • shadcn/ui + Radix UI: Unstyled, accessible primitives styled with Tailwind CSS
  • Headless UI: Accessible components from the Tailwind CSS team
  • React Aria (Adobe): Low-level accessibility hooks for custom components

Start with accessible primitives, then maintain compliance as you customize.


This is a shorter version of a guide I wrote at Frontkit Studio. The full post has the complete walkthrough.

Top comments (0)