The Complete Accessibility Checklist for Web Apps
Building accessible web applications ensures everyone can use your product. This comprehensive WCAG 2.2 compliant checklist covers essential accessibility requirements for modern web development.
WCAG 2.2 Overview
WCAG 2.2 builds on WCAG 2.1 with new success criteria addressing:
- Focus Not Obscured - Focus indicators must not be hidden by other content
- Target Size (Minimum) - Interactive elements need adequate touch/click targets
- Dragging Movements - Dragging interactions must have single-pointer alternatives
- Accessible Authentication - Alternatives to cognitive function tests
- Consistent Help - Help information must be consistently located
- Redundant Entry - Avoid re-entering information across steps
Semantic HTML & ARIA Landmarks
Use semantic HTML elements and ARIA landmarks for screen reader navigation:
<!-- Primary landmarks -->
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/products">Products</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Article Title</h1>
<section aria-labelledby="overview">
<h2 id="overview">Overview</h2>
<p>Content...</p>
</section>
</article>
</main>
<aside aria-label="Related content">
<!-- Sidebar content -->
</aside>
<footer>
<p>Copyright 2024</p>
</footer>
<!-- Avoid generic divs -->
<!-- ❌ Bad -->
<div class="header">
<div class="nav">
<div class="link">Home</div>
</div>
</div>
Keyboard Navigation & Focus Management
All interactive elements must be keyboard accessible with visible focus indicators:
// Custom button with proper keyboard support
function CustomButton({ onClick, children }) {
const handleKeyDown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick(e);
}
};
return (
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={handleKeyDown}
className="custom-button"
>
{children}
</div>
);
}
// Focus management for modals (WCAG 2.2 Focus Not Obscured)
function Modal({ isOpen, onClose, children }) {
const dialogRef = useRef(null);
const previousFocus = useRef(null);
useEffect(() => {
if (isOpen) {
// Store previous focus
previousFocus.current = document.activeElement;
// Focus dialog
dialogRef.current?.focus();
// Trap focus within modal
const focusableElements = dialogRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements?.length) {
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const handleTabKey = (e) => {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
};
document.addEventListener('keydown', handleTabKey);
return () => {
document.removeEventListener('keydown', handleTabKey);
// Restore focus on close
previousFocus.current?.focus();
};
}
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
tabIndex={-1}
className="modal"
>
{children}
<button onClick={onClose} aria-label="Close dialog">×</button>
</div>
);
}
Target Size (WCAG 2.2 New)
Interactive elements must be at least 24×24 CSS pixels (44×44px recommended):
/* Minimum target size */
button, [role="button"], input, select, textarea {
min-width: 44px;
min-height: 44px;
}
/* For smaller elements, add padding */
.small-button {
padding: 8px; /* Total 40px with border */
min-width: 24px;
min-height: 24px;
}
ARIA Labels, Roles & States
Use ARIA strategically - prefer semantic HTML over ARIA when possible:
<!-- Screen reader text (visually hidden) -->
<a href="#main" class="sr-only focusable">Skip to main content</a>
<main id="main">...</main>
<!-- Icon buttons need aria-label -->
<button aria-label="Close dialog" class="close-button">
<svg aria-hidden="true"><!-- icon --></svg>
</button>
<!-- Status messages -->
<div role="status" aria-live="polite" aria-atomic="true">
File uploaded successfully!
</div>
<!-- Loading states -->
<div aria-live="polite" aria-busy="true" role="status">
<div aria-hidden="true">Loading...</div>
<progress value="50" max="100" aria-label="Loading progress">50%</progress>
</div>
<!-- Complex widgets -->
<div role="tablist" aria-label="Product categories">
<button role="tab" aria-selected="true" aria-controls="electronics">
Electronics
</button>
<div role="tabpanel" id="electronics" aria-labelledby="electronics-tab">
<!-- Content -->
</div>
</div>
<!-- Form validation -->
<input
type="email"
aria-invalid="true"
aria-describedby="email-error"
required
/>
<div id="email-error" role="alert">
Please enter a valid email address
</div>
Focus Appearance (WCAG 2.2 AAA)
Focus indicators must be clearly visible and meet contrast requirements:
/* High contrast focus indicators */
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
}
/* Alternative: border-based focus */
.focusable:focus-visible {
border: 2px solid #005fcc;
box-shadow: 0 0 0 1px #005fcc;
}
Focus Management
function Dialog({ isOpen, onClose, children }) {
const dialogRef = useRef(null);
const previousFocus = useRef(null);
useEffect(() => {
if (isOpen) {
// Store previous focus
previousFocus.current = document.activeElement;
// Focus dialog
dialogRef.current?.focus();
return () => {
// Restore focus on close
previousFocus.current?.focus();
};
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
tabIndex={-1}
>
{children}
<button onClick={onClose}>Close</button>
</div>
);
}
Color Contrast
WCAG AA requires:
- Normal text: 4.5:1 contrast ratio
- Large text (18pt+): 3:1 contrast ratio
/* Good - 7:1 ratio */
.text {
color: #000000;
background: #ffffff;
}
/* Bad - 2:1 ratio */
.text {
color: #cccccc;
background: #ffffff;
}
Form Accessibility & Authentication
Forms must be fully accessible with proper labeling and validation:
<form action="/api/contact" method="post">
<!-- Explicit labels -->
<div>
<label for="name">Full Name</label>
<input
type="text"
id="name"
name="name"
autocomplete="name"
required
aria-describedby="name-hint"
/>
<span id="name-hint">Enter your full legal name</span>
</div>
<!-- Group related fields -->
<fieldset>
<legend>Contact Preference</legend>
<input
type="radio"
id="email-pref"
name="contact-method"
value="email"
checked
/>
<label for="email-pref">Email</label>
<input
type="radio"
id="phone-pref"
name="contact-method"
value="phone"
/>
<label for="phone-pref">Phone</label>
</fieldset>
<!-- Validation feedback -->
<div aria-live="polite">
<span id="email-error" role="alert" aria-atomic="true">
<!-- Error message inserted here -->
</span>
</div>
<!-- Submit button -->
<button type="submit" aria-describedby="submit-help">
Send Message
</button>
<div id="submit-help">
Your information will be kept confidential
</div>
</form>
Accessible Authentication (WCAG 2.2 New)
Provide alternatives to cognitive function tests:
<!-- Situation A: Provide alternatives to memory tests -->
<div class="auth-options">
<h3>Choose your authentication method:</h3>
<button onclick="usePassword()">
Use Password
</button>
<button onclick="useBiometric()">
Use Biometric (Fingerprint/Face)
</button>
<button onclick="useWebAuthn()">
Use Security Key
</button>
<!-- Avoid: Pure memory/cognitive tests -->
<!-- ❌ Bad: "What was the name of your first pet?" -->
</div>
<!-- Situation B: Object recognition -->
<div class="captcha-alternative">
<p>Select all images containing traffic lights:</p>
<div class="image-grid" role="group" aria-label="Select traffic lights">
<!-- Images with proper alt text and keyboard support -->
</div>
</div>
Dragging Movements (WCAG 2.2 New)
Provide single-pointer alternatives for drag operations:
<!-- Draggable list with keyboard support -->
<div class="sortable-list">
<div class="list-item" draggable="true">
<span>Item 1</span>
<button aria-label="Move up">↑</button>
<button aria-label="Move down">↓</button>
<button aria-label="Remove">×</button>
</div>
</div>
<!-- Drag alternative: explicit controls -->
<div class="reorder-controls">
<select aria-label="Move item to position">
<option>Move to top</option>
<option>Move up one</option>
<option>Move down one</option>
<option>Move to bottom</option>
</select>
</div>
Images and Media
<!-- Meaningful images -->
<img src="chart.png" alt="Sales increased 50% in Q4 2024" />
<!-- Decorative images -->
<img src="decorative.png" alt="" role="presentation" />
<!-- Video captions -->
<video>
<source src="video.mp4" />
<track kind="captions" src="captions.vtt" srclang="en" label="English" />
</video>
Media & Dynamic Content
Ensure all media and dynamic content is accessible:
<!-- Images with meaningful alt text -->
<img
src="chart.png"
alt="Sales increased 50% in Q4 2024, with mobile sales up 75%"
width="400"
height="300"
/>
<!-- Decorative images -->
<img
src="decorative-pattern.png"
alt=""
role="presentation"
/>
<!-- Complex images need detailed descriptions -->
<figure>
<img src="diagram.png" alt="User authentication flow diagram" />
<figcaption>
Figure 1: Authentication flow showing login, 2FA, and session creation
</figcaption>
<details>
<summary>Long description</summary>
<p>The diagram shows a user icon connecting to a login form...</p>
</details>
</figure>
<!-- Video with captions and transcripts -->
<video controls preload="metadata">
<source src="tutorial.mp4" type="video/mp4" />
<track
kind="captions"
src="tutorial-captions.vtt"
srclang="en"
label="English"
default
/>
<track
kind="subtitles"
src="tutorial-subtitles-es.vtt"
srclang="es"
label="Español"
/>
<!-- Fallback -->
<p>Your browser doesn't support video. <a href="tutorial.mp4">Download</a></p>
</video>
<!-- Provide transcript -->
<details>
<summary>Video Transcript</summary>
<p>[00:00] Welcome to our accessibility tutorial...</p>
</details>
Comprehensive Testing Checklist
Automated Testing
- axe DevTools - Run automated accessibility audits
- Lighthouse - Check accessibility score (target: 90+)
- WAVE - Web accessibility evaluation
- ARC Toolkit - Comprehensive accessibility testing
- eslint-plugin-jsx-a11y - Lint React components
Manual Testing
- Keyboard Navigation - Tab through all interactive elements
- Screen Readers - Test with NVDA, JAWS, VoiceOver, TalkBack
- Focus Management - Verify focus indicators and tab order
- Zoom Testing - 200% zoom, 400% zoom
- Color Contrast - Use contrast checkers (4.5:1 minimum)
- Touch Targets - Minimum 44×44px for touch interfaces
- Reduced Motion - Test
prefers-reduced-motionsettings
WCAG 2.2 Specific Tests
- Focus Not Obscured - Focus indicators never hidden
- Target Size - All interactive elements meet minimum size
- Dragging Movements - Single-pointer alternatives provided
- Accessible Authentication - No pure cognitive tests
- Consistent Help - Help information consistently located
User Testing
- Real User Testing - Include users with disabilities
- Assistive Technology - Test with various screen readers
- Mobile Accessibility - Test with TalkBack, VoiceOver on mobile
- Cognitive Accessibility - Test with users who need extra time
Modern Accessibility Tools (2024)
Development Tools
- axe-core - JavaScript accessibility testing library
- @axe-core/playwright - Automated testing in Playwright
- cypress-axe - Cypress accessibility testing
- jest-axe - Unit test accessibility assertions
- storybook-addon-a11y - Storybook accessibility testing
Design Tools
- Stark - Contrast checking and accessibility design
- Color Contrast Analyzer - Adobe/Apple contrast tools
- Accessibility Insights - Microsoft accessibility tools
- WAVE Browser Extension - Real-time accessibility feedback
Screen Readers & Testing
- NVDA (Windows) - Free, open-source screen reader
- JAWS (Windows) - Professional screen reader
- VoiceOver (macOS/iOS) - Built-in Apple screen reader
- TalkBack (Android) - Google screen reader
- Narrator (Windows) - Microsoft screen reader
CI/CD Integration
# GitHub Actions accessibility testing
- name: Accessibility Tests
run: |
npx axe-core --url https://your-site.com
npx lighthouse --accessibility https://your-site.com
- name: Visual Regression
run: |
npx playwright test --grep "accessibility"
Progressive Enhancement Strategy
Build accessible by default, enhance progressively:
// Feature detection for enhanced interactions
const supportsIntersectionObserver = 'IntersectionObserver' in window;
if (supportsIntersectionObserver) {
// Enhanced lazy loading
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadContent(entry.target);
}
});
});
} else {
// Fallback: load all content
loadAllContent();
}
// Safe keyboard event handling
function handleKeyPress(event) {
// Always prevent default for custom behavior
switch (event.key) {
case 'Enter':
case ' ': // Space
event.preventDefault();
performAction();
break;
case 'Escape':
event.preventDefault();
closeModal();
break;
}
}
Legal & Compliance Considerations
WCAG Conformance Levels
- A - Basic accessibility (minimum legal requirement)
- AA - Enhanced accessibility (recommended for most sites)
- AAA - Highest accessibility (required for government/mission-critical)
Industry Standards
- Section 508 (US Government)
- EN 301 549 (European accessibility standard)
- AODA (Ontario Accessibility for Ontarians with Disabilities Act)
- DDA (UK Disability Discrimination Act)
Documentation Requirements
- Accessibility Statement
- Accessibility Conformance Report (ACR)
- Feedback/Contact Information for accessibility issues
- Regular accessibility audits (annual minimum)
Resources & Learning
Official Standards
- WCAG 2.2 Guidelines - Latest accessibility standard
- ARIA Authoring Practices Guide - ARIA implementation patterns
- WAI-ARIA Specification - ARIA technical specification
Learning Resources
- WebAIM - Comprehensive accessibility articles
- Deque University - Free accessibility courses
- Accessibility Guidelines Working Group - WCAG development
- A11y Project - Practical accessibility resources
Communities
- WAI Interest Group - Accessibility discussions
- CSS Accessibility Community Group
- Web Accessibility Initiative - W3C accessibility resources
Conclusion
Accessibility is not a checkbox - it's an ongoing commitment to inclusive design. WCAG 2.2 brings important updates that address modern user needs, from mobile accessibility to cognitive load reduction.
Start with semantic HTML, proper keyboard navigation, and automated testing. Then progressively enhance with ARIA, advanced focus management, and user testing. Remember: the best accessibility happens when accessibility is designed in from the start, not bolted on at the end.
Your users will thank you for creating experiences that work for everyone, regardless of ability. What accessibility challenge are you tackling next?