WCAG 2.1 Accessibility Testing: A Developer's Practical Guide
WCAG 2.1 — the Web Content Accessibility Guidelines, version 2.1 — is the international standard for web accessibility. Published by the W3C in 2018, it defines how to make web content accessible to people with disabilities. For developers, the challenge isn't understanding the principles (they're sensible) — it's translating the abstract success criteria into concrete testing practices.
This guide cuts through the specification language and gives you a practical testing approach: what WCAG requires, which parts automated tools handle, which require manual testing, and the most common violations with their fixes.
The Three Levels of WCAG Compliance
WCAG organizes success criteria into three levels:
Level A — The minimum. Violations at this level create barriers so severe they make a page completely unusable for some users. Think: images with no alt text, form fields with no labels, pages that can't be navigated with a keyboard at all.
Level AA — The standard compliance target. Required by most legislation (ADA, Section 508, EN 301 549, AODA). Adds color contrast requirements, consistent navigation, error identification, and more. When organizations say "we comply with WCAG," they mean WCAG 2.1 AA.
Level AAA — Maximum. Adds requirements that aren't feasible for all content types (sign language interpretation, extended audio description). Rarely required across an entire site; often achieved selectively for specific high-importance pages.
The baseline you should target: WCAG 2.1 AA. This is the legal requirement in most jurisdictions and the right technical target for a professional web application.
The Four Principles (POUR)
Every WCAG success criterion falls under one of four principles:
Perceivable
Content must be presentable in ways users can perceive. Users who can't see must have text alternatives. Users who can't hear must have captions. Content must be separable from its presentation.
Key success criteria:
- 1.1.1 Non-text content (A) — Images, icons, and controls need text alternatives
- 1.3.1 Info and relationships (A) — Structure (headings, lists, tables) must be programmatically determinable
- 1.3.4 Orientation (AA) — Content must not lock to a single screen orientation
- 1.4.1 Use of color (A) — Color alone cannot convey information
- 1.4.3 Contrast minimum (AA) — 4.5:1 ratio for normal text, 3:1 for large text
- 1.4.4 Resize text (AA) — Text must be resizable to 200% without assistive technology
Operable
Interface components must be operable by all users. Everything clickable must also be keyboard-accessible. Users must have enough time. Pages must not cause seizures.
Key success criteria:
- 2.1.1 Keyboard (A) — All functionality available via keyboard
- 2.1.2 No keyboard trap (A) — Focus must not be trapped inside a component
- 2.4.1 Bypass blocks (A) — Skip navigation link required
- 2.4.2 Page titled (A) — Pages must have descriptive titles
- 2.4.3 Focus order (A) — Focus order must preserve meaning and operability
- 2.4.4 Link purpose (A) — Link text describes its destination
- 2.4.7 Focus visible (AA) — Keyboard focus indicator must be visible
- 2.5.3 Label in name (A) — Visible label must be in the accessible name
Understandable
Content and operation must be understandable. Language must be identified. Navigation must be consistent. Errors must be identified and explained.
Key success criteria:
- 3.1.1 Language of page (A) —
<html lang="...">must be set - 3.2.1 On focus (A) — Focus must not change context unexpectedly
- 3.3.1 Error identification (A) — Errors must be described in text
- 3.3.2 Labels or instructions (A) — Labels provided for input
- 3.3.3 Error suggestion (AA) — Error correction hints provided when possible
- 3.3.4 Error prevention (AA) — For legal/financial submissions, reversible, checked, or confirmed
Robust
Content must be robust enough to work with current and future assistive technologies. Standard HTML semantics must be used correctly.
Key success criteria:
- 4.1.1 Parsing (A) — Valid HTML, no duplicate IDs
- 4.1.2 Name, role, value (A) — All UI components have accessible names and correct roles
- 4.1.3 Status messages (AA) — Status messages must be programmatically determinable
What Automated Tools Can and Cannot Catch
Automated tools (axe-core, Pa11y, Lighthouse) catch roughly 30–40% of WCAG 2.1 AA violations. This is a useful but incomplete baseline.
Automated tools catch reliably:
| Issue | Relevant Criterion |
|---|---|
| Missing alt text | 1.1.1 |
| Missing form labels | 1.3.1 |
| Color contrast ratios | 1.4.3 |
Missing lang attribute |
3.1.1 |
| Missing page title | 2.4.2 |
| Invalid ARIA attributes | 4.1.2 |
| Duplicate IDs | 4.1.1 |
| Incorrect ARIA role hierarchies | 4.1.2 |
<input type="image"> without alt |
1.1.1 |
| Empty links and buttons | 4.1.2 |
| Missing skip links | 2.4.1 |
Automated tools cannot reliably catch:
| Issue | Relevant Criterion | Why |
|---|---|---|
| Meaningful alt text | 1.1.1 | Tools check presence, not quality |
| Logical reading order | 1.3.2 | Context-dependent |
| Focus order reasonableness | 2.4.3 | Requires human judgment |
| Error message quality | 3.3.3 | Content quality, not structure |
| Consistent navigation | 3.2.3 | Requires cross-page awareness |
| Screen reader announcement quality | 4.1.2 | Requires actual SR testing |
| Cognitive load | Not in WCAG 2.1 | Qualitative |
The practical implication: Use automated tools as your baseline and regression gate. Then supplement with manual keyboard testing, screen reader testing, and periodic expert review.
Keyboard Navigation Testing
Keyboard accessibility is one of the most impactful areas to test and one that automated tools largely miss. Here's a systematic manual test:
Basic Keyboard Navigation Checklist
Open your page in Chrome, click somewhere outside the page to ensure the browser isn't focused, then:
- Press Tab repeatedly — every interactive element (links, buttons, form fields, custom widgets) should receive a visible focus indicator. Nothing should be skipped.
- Check focus order — focus should move logically (usually left-to-right, top-to-bottom following visual layout). After a modal opens, focus should move into the modal. After it closes, focus should return to the trigger.
- Press Enter on focused links and buttons — they must activate.
- Test dropdowns with arrow keys — native
<select>elements work. Custom dropdowns must implement arrow key navigation or they fail 2.1.1. - Press Escape — modals, dropdown menus, and overlays must close.
- Look for focus traps — press Tab repeatedly inside a modal. Focus should cycle within the modal, not escape to the page behind it (2.1.2).
- Check the skip link — focus the page from the browser address bar, press Tab once. The first focusable element should be a skip link to main content.
Testing Focus Visibility
Focus indicators are required by 2.4.7. Many designs suppress them with:
/* This violates WCAG 2.4.7 — never do this */
*:focus {
outline: none;
}The acceptable approach is a custom focus indicator that's still visible:
/* Custom focus style — meets WCAG 2.4.7 */
:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
}
/* Suppress for mouse users only (modern approach) */
:focus:not(:focus-visible) {
outline: none;
}Test by tabbing through the page in Chrome with DevTools open. Verify every interactive element has a visible indicator when focused.
Color Contrast Testing
WCAG 1.4.3 requires:
- 4.5:1 minimum for normal text (under 18pt / 14pt bold)
- 3:1 minimum for large text (18pt+ / 14pt+ bold)
- 3:1 minimum for UI components (button borders, checkbox outlines, icons)
Automated contrast checking
axe-core and Pa11y check contrast for text that exists in the DOM with resolved CSS. They miss:
- Text in images (check manually)
- Text that appears only on hover
- Text with gradients or background images behind it
Manual contrast checking
Use the browser's built-in tools:
Chrome DevTools:
- Open DevTools → Elements
- Click on a text element
- In the Styles panel, click the color swatch
- The color picker shows the contrast ratio and whether it passes AA/AAA
Browser extensions:
- Colour Contrast Analyser (desktop app, works anywhere including images)
- axe DevTools browser extension — highlights contrast violations
Fixing contrast:
/* Violation: #767676 on white = 4.48:1 (fails AA) */
color: #767676;
/* Fix: #595959 on white = 7.0:1 (passes AA and AAA) */
color: #595959;
/* Violation: white text on #00a0d1 = 2.85:1 */
color: white;
background: #00a0d1;
/* Fix: white text on #0072a0 = 4.52:1 */
background: #0072a0;Use Coolors Contrast Checker or the WebAIM Contrast Checker for quick ratio calculations.
Screen Reader Testing Basics
Screen readers announce content in ways that can't be inferred from visual inspection alone. Even with perfect ARIA markup, the actual announcement may be wrong. Basic screen reader testing requires actually using a screen reader.
Setup
macOS — VoiceOver (built-in):
- Enable: Cmd+F5 or System Settings → Accessibility → VoiceOver
- Turn off: Cmd+F5
- Web navigation: VO key (Control+Option) + Arrow keys, Tab for focusable elements
Windows — NVDA (free):
- Download from nvaccess.org
- Pairs with Firefox or Chrome
- Browse mode: Arrow keys read everything; Tab jumps to interactive elements
Windows — JAWS (commercial, industry standard):
- Common in enterprise and government contexts
- Free 40-minute trial mode
iOS — VoiceOver:
- Enable: Settings → Accessibility → VoiceOver
- Swipe right to move forward, double-tap to activate
What to Test
1. Page title announced on load The page title (from <title>) is the first thing announced when a page loads. It should uniquely identify the current page.
2. Heading navigation VoiceOver: VO+Command+H to navigate headings NVDA: H to navigate headings in browse mode
Verify the heading structure makes the page navigable without visual context.
3. Form field announcements Click or tab to each form field. The screen reader should announce the label, field type, and any error. Test error messages — they should be announced when triggered.
4. Modal dialog behavior When a modal opens, the screen reader should announce the dialog title and move focus inside it. When it closes, focus returns to the trigger. Test this explicitly.
5. Dynamic content (live regions) Content that updates without a page load (notifications, form feedback, search results) needs ARIA live regions to be announced:
<!-- Polite: reads after user pauses -->
<div aria-live="polite" aria-atomic="true" id="status">
Search returned 42 results
</div>
<!-- Assertive: reads immediately (use sparingly) -->
<div role="alert" aria-live="assertive">
Error: Your session has expired
</div>ARIA Landmark Roles
Landmarks let screen reader users jump to major page sections. Every page should have:
| Role | HTML element | Requirement |
|---|---|---|
banner |
<header> |
One per page (top-level header) |
navigation |
<nav> |
All nav elements; label unique ones with aria-label |
main |
<main> |
One per page |
complementary |
<aside> |
Sidebar content |
contentinfo |
<footer> |
One per page (top-level footer) |
search |
<search> or role="search" |
Search forms |
Multiple navs need distinguishing labels:
<nav aria-label="Main">
<!-- primary navigation -->
</nav>
<nav aria-label="Breadcrumb">
<!-- breadcrumb trail -->
</nav>
<footer>
<nav aria-label="Footer">
<!-- footer links -->
</nav>
</footer>NVDA users can press D to navigate between landmarks. Without landmarks, navigating a large page by headings or reading sequentially is their only option.
Practical Testing Checklist
Use this before shipping any new page or significant feature:
Automated (run in CI)
- axe-core finds no violations (run via Playwright or Cypress)
- Pa11y CI finds no errors above threshold
- Lighthouse accessibility score ≥ 90
Manual — Keyboard
- All interactive elements reachable via Tab
- Visible focus indicator on every interactive element
- Modals trap focus correctly
- Escape closes modals and menus
- Custom widgets support arrow key navigation
- Skip link present and functional
Manual — Visual
- Color contrast verified for all text (not just automated check)
- Information not conveyed by color alone
- Error messages visible and descriptive
Manual — Screen Reader (spot check)
- Page title descriptive
- Heading hierarchy makes sense when read aloud
- Form fields announce label + type
- Error messages announced
- Dynamic content announced via live regions
Structural
<html lang="en">(or appropriate language)- Unique
<title>on every page - Heading hierarchy starts with h1, no skipped levels
- All images have appropriate alt text
- Tables have
scopeattributes on headers
Common Violations and Fixes
1. Auto-playing media (WCAG 1.4.2)
<!-- Violation -->
<video autoplay>...</video>
<!-- Fix: provide controls, don't autoplay sound -->
<video controls>...</video>
<!-- Or if autoplay required, mute it -->
<video autoplay muted controls>...</video>2. Timeout warnings not given (WCAG 2.2.1)
If a session expires after inactivity, users must be warned before timeout and given time to extend:
// Warn 2 minutes before timeout
function startTimeoutWarning(sessionDuration, warningBefore = 120000) {
setTimeout(() => {
// Show accessible warning dialog with extend/logout options
showTimeoutWarning();
}, sessionDuration - warningBefore);
}3. Focus not managed after route change in SPAs
Single-page apps often don't move focus after navigation, leaving screen reader users stranded at the old content:
// React Router — focus management after navigation
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
function FocusManager() {
const location = useLocation();
const mainRef = useRef<HTMLElement>(null);
useEffect(() => {
// Move focus to main content after each route change
mainRef.current?.focus();
}, [location.pathname]);
return <main id="main" tabIndex={-1} ref={mainRef}>...</main>;
}4. aria-label conflicts with visible text (WCAG 2.5.3)
The accessible name must contain the visible label text:
<!-- Violation: aria-label doesn't match visible text -->
<button aria-label="Submit application">Send</button>
<!-- Fix: match the visible text -->
<button aria-label="Send application">Send</button>
<!-- Or better — just use the button text -->
<button>Send application</button>5. Status messages not announced (WCAG 4.1.3)
<!-- Violation: success message appears visually but isn't announced -->
<div class="success-message" id="save-status">
Changes saved successfully
</div>
<!-- Fix: aria-live region -->
<div
class="success-message"
id="save-status"
aria-live="polite"
aria-atomic="true"
>
Changes saved successfully
</div>Setting Up an Accessibility Testing Workflow
A practical workflow for a development team:
In development (local):
- Browser extension (axe DevTools or Accessibility Insights) for quick checks while building
jest-axein component tests for unit-level coverage
In CI (every PR):
@axe-core/playwrightor axe-cypress in E2E test suite- Lighthouse CI for aggregate score tracking
- Pa11y-CI for URL-based batch scanning of key pages
Periodic manual review (monthly or on major features):
- Keyboard navigation walkthrough of critical user flows
- Screen reader test with VoiceOver (macOS) or NVDA (Windows)
- Color contrast audit of new design system tokens
Before major releases:
- External accessibility audit by a specialist for anything with legal exposure
The manual testing is where you catch what automation misses: focus order that's technically valid but illogical, error messages that are marked up correctly but confusing, or navigation that works with a mouse but fails the keyboard test because of an unexpected focus order.
Accessibility isn't a checklist you complete once. It's a discipline you practice continuously. Automated testing in CI ensures you don't regress on what's catchable. Manual testing ensures the things that require judgment don't slip through. Both are required for WCAG 2.1 AA compliance — and more importantly, for building software that actually works for the people who depend on these standards.