Accessibility Auditing
Audit and improve accessibility for WCAG 2.1 AA compliance.
Process
- Identify scope - Which pages/components to audit?
- Automated checks - Run linting and browser tools
- Manual testing - Keyboard navigation, screen reader
- Document issues - Severity, location, fix
- Implement fixes - Priority order
WCAG 2.1 Checklist
Perceivable
Images
// Required: alt text for informational images
<img src={article.image} alt={article.imageAlt || article.title} />
// Decorative images: empty alt
<img src="/decorative-border.png" alt="" role="presentation" />
Color Contrast
- Normal text: 4.5:1 minimum
- Large text (18px+): 3:1 minimum
- Use theme CSS variables which meet contrast requirements
Video/Audio
// Captions for video content
<video>
<track kind="captions" src="/captions.vtt" srclang="en" label="English" />
</video>
Operable
Keyboard Navigation
// All interactive elements focusable
<button me</button> // Good: button is focusable
// Custom elements need tabindex
<div role="button" tabIndex={0}
Click me
</div>
// Skip links for main content
<a href="#main-content" className="sr-only focus:not-sr-only">
Skip to main content
</a>
Focus Indicators
// Visible focus states
className="focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
Focus Trapping (Modals)
// Trap focus within modal
useEffect(() => {
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusableElements[0];
const last = focusableElements[focusableElements.length - 1];
function handleTab(e: KeyboardEvent) {
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();
}
}
}
// ...
}, []);
Understandable
Form Labels
// Always associate labels with inputs
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" />
// Or use aria-label for icon-only buttons
<button aria-label="Close modal">
<XIcon />
</button>
Error Messages
// Associate errors with inputs
<input
id="email"
aria-invalid={!!error}
aria-describedby={error ? "email-error" : undefined}
/>
{error && (
<p id="email-error" role="alert" className="text-red-500">
{error}
</p>
)}
Language
// Declare page language
<html lang="en">
Robust
Semantic HTML
// Use appropriate elements
<nav>...</nav> // Navigation
<main>...</main> // Main content
<article>...</article> // Article content
<aside>...</aside> // Sidebar
<header>...</header> // Header
<footer>...</footer> // Footer
// Use headings in order
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
ARIA Roles and States
// Live regions for dynamic content
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
// Loading states
<button aria-busy={loading} disabled={loading}>
{loading ? 'Saving...' : 'Save'}
</button>
// Expanded/collapsed
<button aria-expanded={isOpen} aria-controls="menu">
Menu
</button>
<div id="menu" hidden={!isOpen}>...</div>
Common Issues in This Project
Admin Tables
- Ensure sortable columns are keyboard accessible
- Add scope attributes to table headers
- Announce sort changes with aria-live
Media Library
- All images need alt text (use title field)
- Video players need keyboard controls
- Upload progress announced to screen readers
Forms
- All fields need visible labels
- Required fields marked with aria-required
- Error states linked with aria-describedby
Navigation
- Mobile menu needs aria-expanded
- Current page indicated with aria-current="page"
- Dropdown menus keyboard navigable
Testing Tools
# ESLint a11y plugin (add to eslint.config.mjs)
npm install eslint-plugin-jsx-a11y --save-dev
# Playwright a11y testing
npm install @axe-core/playwright --save-dev
Output
Provide accessibility audit results:
- Issue list with severity (Critical/Serious/Moderate/Minor)
- WCAG criterion violated
- Location (file:line or selector)
- Recommended fix with code example