Building accessible web applications isn't just about compliance—it's about creating experiences that work for everyone. With WCAG 2.2 now the standard, this guide covers practical implementation strategies for developers.
Understanding WCAG 2.2
WCAG 2.2 is organized around four principles (POUR):
- Perceivable: Information must be presentable in ways users can perceive
- Operable: Interface components must be operable by all users
- Understandable: Information and operation must be understandable
- Robust: Content must be robust enough for assistive technologies
Essential Accessibility Patterns
Semantic HTML
<!-- Bad: div soup -->
<div class="header">
<div class="nav">
<div class="nav-item">Home</div>
</div>
</div>
<!-- Good: semantic elements -->
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
</header>
Accessible Forms
<form>
<div class="form-group">
<label for="email">Email address</label>
<input
type="email"
id="email"
name="email"
aria-describedby="email-help email-error"
aria-invalid="true"
required
>
<p id="email-help" class="help-text">
We'll never share your email.
</p>
<p id="email-error" class="error" role="alert">
Please enter a valid email address.
</p>
</div>
<button type="submit">Subscribe</button>
</form>
Keyboard Navigation
// Custom dropdown with keyboard support
function Dropdown({ options, onChange }) {
const [isOpen, setIsOpen] = useState(false);
const [focusIndex, setFocusIndex] = useState(0);
const handleKeyDown = (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setFocusIndex(i => Math.min(i + 1, options.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setFocusIndex(i => Math.max(i - 1, 0));
break;
case 'Enter':
case ' ':
e.preventDefault();
onChange(options[focusIndex]);
setIsOpen(false);
break;
case 'Escape':
setIsOpen(false);
break;
}
};
return (
<div
role="listbox"
tabIndex={0}
onKeyDown={handleKeyDown}
aria-activedescendant={\`option-\${focusIndex}\`}
>
{options.map((option, i) => (
<div
key={option.value}
id={\`option-\${i}\`}
role="option"
aria-selected={i === focusIndex}
>
{option.label}
</div>
))}
</div>
);
}
Color and Contrast
WCAG 2.2 requires:
- Normal text: 4.5:1 contrast ratio
- Large text (18pt+): 3:1 contrast ratio
- UI components: 3:1 against adjacent colors
/* CSS custom properties for accessible colors */
:root {
--text-primary: #1a1a1a; /* 15:1 on white */
--text-secondary: #595959; /* 7:1 on white */
--background: #ffffff;
--accent: #0066cc; /* 5.9:1 on white */
--error: #d32f2f; /* 4.8:1 on white */
}
ARIA Patterns
Live Regions
<!-- Announce dynamic content changes -->
<div aria-live="polite" aria-atomic="true">
{{ statusMessage }}
</div>
<!-- For urgent alerts -->
<div role="alert">
Your session will expire in 2 minutes.
</div>
Modal Dialogs
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Confirm deletion</h2>
<p id="dialog-description">
This action cannot be undone.
</p>
<button onClick={onConfirm}>Delete</button>
<button onClick={onCancel}>Cancel</button>
</div>
Testing Tools
- axe DevTools: Browser extension for automated testing
- WAVE: Web accessibility evaluation tool
- Lighthouse: Built into Chrome DevTools
- Screen readers: NVDA (Windows), VoiceOver (macOS/iOS)
// Automated testing with jest-axe
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('form is accessible', async () => {
const { container } = render(<ContactForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Accessibility is a journey, not a destination. Start with the basics, test regularly, and continuously improve.
Comments (0)
Leave a Comment
No comments yet. Be the first to share your thoughts!