---
name: accessibility-compliance
description: Implement WCAG 2.1/2.2 accessibility standards, screen reader compatibility, keyboard navigation, and a11y testing. Use when building inclusive web applications, ensuring regulatory compliance, or improving user experience for people with disabilities.
---
# Accessibility Compliance
## Overview
Implement comprehensive accessibility features following WCAG guidelines to ensure your application is usable by everyone, including people with disabilities.
## When to Use
- Building public-facing web applications
- Ensuring WCAG 2.1/2.2 AA or AAA compliance
- Supporting screen readers (NVDA, JAWS, VoiceOver)
- Implementing keyboard-only navigation
- Meeting ADA, Section 508, or similar regulations
- Improving SEO and overall user experience
- Conducting accessibility audits
## Key Principles (POUR)
1. **Perceivable** - Information must be presentable to users in ways they can perceive
2. **Operable** - Interface components must be operable
3. **Understandable** - Information and operation must be understandable
4. **Robust** - Content must be robust enough to be interpreted by assistive technologies
## Implementation Examples
### 1. **Semantic HTML with ARIA**
```html
Submit
Submit
Toggle Feature
```
### 2. **React Component with Accessibility**
```typescript
import React, { useRef, useEffect, useState } from 'react';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
const AccessibleModal: React.FC = ({
isOpen,
onClose,
title,
children
}) => {
const modalRef = useRef(null);
const previousFocusRef = useRef(null);
useEffect(() => {
if (isOpen) {
// Save previous focus
previousFocusRef.current = document.activeElement as HTMLElement;
// Focus modal
modalRef.current?.focus();
// Trap focus within modal
const trapFocus = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', trapFocus);
return () => {
document.removeEventListener('keydown', trapFocus);
// Restore previous focus
previousFocusRef.current?.focus();
};
}
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
e.stopPropagation()}
>
{title}
×
{children}
);
};
export default AccessibleModal;
```
### 3. **Keyboard Navigation Handler**
```typescript
// Keyboard navigation utilities
export const KeyboardNavigation = {
// Handle arrow key navigation in lists
handleListNavigation: (event: KeyboardEvent, items: HTMLElement[]) => {
const currentIndex = items.findIndex(item =>
item === document.activeElement
);
let nextIndex: number;
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
nextIndex = Math.min(currentIndex + 1, items.length - 1);
items[nextIndex]?.focus();
break;
case 'ArrowUp':
event.preventDefault();
nextIndex = Math.max(currentIndex - 1, 0);
items[nextIndex]?.focus();
break;
case 'Home':
event.preventDefault();
items[0]?.focus();
break;
case 'End':
event.preventDefault();
items[items.length - 1]?.focus();
break;
}
},
// Make element keyboard accessible
makeAccessible: (
element: HTMLElement,
onClick: () => void
): void => {
element.setAttribute('tabindex', '0');
element.setAttribute('role', 'button');
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
});
}
};
```
### 4. **Color Contrast Validator**
```python
from typing import Tuple
import math
def hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
"""Convert hex color to RGB."""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def calculate_luminance(rgb: Tuple[int, int, int]) -> float:
"""Calculate relative luminance."""
def adjust(color: int) -> float:
c = color / 255.0
if c <= 0.03928:
return c / 12.92
return math.pow((c + 0.055) / 1.055, 2.4)
r, g, b = rgb
return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b)
def calculate_contrast_ratio(color1: str, color2: str) -> float:
"""Calculate WCAG contrast ratio between two colors."""
lum1 = calculate_luminance(hex_to_rgb(color1))
lum2 = calculate_luminance(hex_to_rgb(color2))
lighter = max(lum1, lum2)
darker = min(lum1, lum2)
return (lighter + 0.05) / (darker + 0.05)
def check_wcag_compliance(
foreground: str,
background: str,
level: str = 'AA',
large_text: bool = False
) -> dict:
"""Check if color combination meets WCAG standards."""
ratio = calculate_contrast_ratio(foreground, background)
# WCAG 2.1 requirements
requirements = {
'AA': {'normal': 4.5, 'large': 3.0},
'AAA': {'normal': 7.0, 'large': 4.5}
}
required_ratio = requirements[level]['large' if large_text else 'normal']
passes = ratio >= required_ratio
return {
'ratio': round(ratio, 2),
'required': required_ratio,
'passes': passes,
'level': level,
'grade': 'Pass' if passes else 'Fail'
}
# Usage
result = check_wcag_compliance('#000000', '#FFFFFF', 'AA', False)
print(f"Contrast ratio: {result['ratio']}:1") # 21:1
print(f"WCAG {result['level']}: {result['grade']}") # Pass
```
### 5. **Screen Reader Announcements**
```typescript
class ScreenReaderAnnouncer {
private liveRegion: HTMLElement;
constructor() {
this.liveRegion = this.createLiveRegion();
}
private createLiveRegion(): HTMLElement {
const region = document.createElement('div');
region.setAttribute('role', 'status');
region.setAttribute('aria-live', 'polite');
region.setAttribute('aria-atomic', 'true');
region.className = 'sr-only';
region.style.cssText = `
position: absolute;
left: -10000px;
width: 1px;
height: 1px;
overflow: hidden;
`;
document.body.appendChild(region);
return region;
}
announce(message: string, priority: 'polite' | 'assertive' = 'polite'): void {
this.liveRegion.setAttribute('aria-live', priority);
// Clear then set message to ensure announcement
this.liveRegion.textContent = '';
setTimeout(() => {
this.liveRegion.textContent = message;
}, 100);
}
cleanup(): void {
this.liveRegion.remove();
}
}
// Usage
const announcer = new ScreenReaderAnnouncer();
// Announce form validation error
announcer.announce('Email field is required', 'assertive');
// Announce successful action
announcer.announce('Item added to cart', 'polite');
```
### 6. **Focus Management**
```typescript
class FocusManager {
private focusableSelectors = [
'a[href]',
'button:not([disabled])',
'textarea:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
getFocusableElements(container: HTMLElement): HTMLElement[] {
return Array.from(
container.querySelectorAll(this.focusableSelectors)
) as HTMLElement[];
}
trapFocus(container: HTMLElement): () => void {
const focusable = this.getFocusableElements(container);
const firstFocusable = focusable[0];
const lastFocusable = focusable[focusable.length - 1];
const handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
lastFocusable.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastFocusable) {
firstFocusable.focus();
e.preventDefault();
}
}
};
container.addEventListener('keydown', handleTabKey);
return () => container.removeEventListener('keydown', handleTabKey);
}
}
```
## Testing Tools and Techniques
### Automated Testing
```typescript
// Jest + Testing Library accessibility tests
import { render, screen } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
describe('Accessibility', () => {
it('should not have accessibility violations', async () => {
const { container } = render( );
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should have proper ARIA labels', () => {
render( {}}>Click me );
const button = screen.getByRole('button', { name: /click me/i });
expect(button).toBeInTheDocument();
});
it('should be keyboard navigable', () => {
const { container } = render( );
const links = screen.getAllByRole('link');
links.forEach(link => {
expect(link).toHaveAttribute('href');
});
});
});
```
## Best Practices
### ✅ DO
- Use semantic HTML elements
- Provide text alternatives for images
- Ensure sufficient color contrast (4.5:1 minimum)
- Support keyboard navigation
- Implement focus management
- Test with screen readers
- Use ARIA attributes correctly
- Provide skip links
- Make forms accessible with labels
- Support text resizing up to 200%
### ❌ DON'T
- Rely solely on color to convey information
- Remove focus indicators
- Use only mouse/touch interactions
- Auto-play media without controls
- Create keyboard traps
- Use positive tabindex values
- Override user preferences
- Hide content only visually that should be hidden from screen readers
## Checklist
- [ ] All images have alt text
- [ ] Color contrast meets WCAG AA standards
- [ ] All interactive elements are keyboard accessible
- [ ] Focus indicators are visible
- [ ] Form inputs have associated labels
- [ ] Error messages are announced to screen readers
- [ ] Skip links are provided
- [ ] Headings follow hierarchical order
- [ ] ARIA attributes are used correctly
- [ ] Content is readable at 200% zoom
- [ ] Tested with keyboard only
- [ ] Tested with screen reader (NVDA, JAWS, VoiceOver)
## Resources
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
- [MDN Accessibility](https://developer.mozilla.org/en-US/docs/Web/Accessibility)
- [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
- [axe DevTools](https://www.deque.com/axe/devtools/)
- [WAVE Browser Extension](https://wave.webaim.org/extension/)
- [Lighthouse Accessibility Audit](https://developers.google.com/web/tools/lighthouse)