--- name: accessibility-compliance description: Implement WCAG 2.1 AA accessibility compliance with ARIA labels, keyboard navigation, screen reader support, and color contrast. Use when ensuring accessibility or fixing a11y issues. allowed-tools: Read, Write, Edit, Bash, Glob --- You implement WCAG 2.1 AA accessibility compliance for the QA Team Portal. ## Requirements from PROJECT_PLAN.md - **Standard:** WCAG 2.1 AA compliance - Keyboard navigation support - Screen reader compatibility - Color contrast standards (4.5:1 for text) - ARIA labels on interactive elements - Focus indicators visible - Accessible forms and error messages ## WCAG 2.1 AA Requirements ### Perceivable 1. Text alternatives for non-text content 2. Captions for audio/video 3. Content can be presented in different ways 4. Color contrast minimum 4.5:1 (text), 3:1 (large text, UI components) ### Operable 1. Keyboard accessible (all functionality) 2. Enough time to read/use content 3. No content that causes seizures (flashing < 3 times per second) 4. Navigation and finding content ### Understandable 1. Readable and understandable text 2. Predictable operation 3. Input assistance (labels, error messages) ### Robust 1. Compatible with assistive technologies 2. Valid HTML 3. Name, role, value for UI components ## Implementation ### 1. Semantic HTML **Use proper HTML5 elements:** ```typescript // ❌ Wrong: Divs for everything
Click me
Home
About
// ✅ Correct: Semantic elements // ✅ Proper document structure

Page Title

Section Title

Content

``` ### 2. ARIA Labels and Roles **Location:** `frontend/src/components/Navigation.tsx` ```typescript export const Navigation = () => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false) return (
) } ``` ### 3. Keyboard Navigation **Focus Management:** ```typescript // frontend/src/components/Modal.tsx import { useEffect, useRef } from 'react' export const Modal = ({ isOpen, onClose, children }) => { const modalRef = useRef(null) const previousFocusRef = useRef(null) useEffect(() => { if (isOpen) { // Store previous focus previousFocusRef.current = document.activeElement as HTMLElement // Focus first focusable element in modal const focusableElements = modalRef.current?.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ) if (focusableElements && focusableElements.length > 0) { (focusableElements[0] as HTMLElement).focus() } // Trap focus inside modal const handleTab = (e: KeyboardEvent) => { if (e.key !== 'Tab') return const focusableContent = modalRef.current?.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ) if (!focusableContent || focusableContent.length === 0) return const firstElement = focusableContent[0] as HTMLElement const lastElement = focusableContent[focusableContent.length - 1] as HTMLElement if (e.shiftKey) { if (document.activeElement === firstElement) { lastElement.focus() e.preventDefault() } } else { if (document.activeElement === lastElement) { firstElement.focus() e.preventDefault() } } } document.addEventListener('keydown', handleTab) return () => { document.removeEventListener('keydown', handleTab) // Restore previous focus previousFocusRef.current?.focus() } } }, [isOpen]) // Close on Escape key useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape' && isOpen) { onClose() } } document.addEventListener('keydown', handleEscape) return () => document.removeEventListener('keydown', handleEscape) }, [isOpen, onClose]) if (!isOpen) return null return (
{/* Backdrop */} ) } ``` **Skip to Main Content:** ```typescript // frontend/src/components/SkipToContent.tsx export const SkipToContent = () => { return ( Skip to main content ) } // Usage in App.tsx
{/* Page content */}
``` ### 4. Form Accessibility **Accessible Form:** ```typescript // frontend/src/components/forms/AccessibleForm.tsx export const LoginForm = () => { const [errors, setErrors] = useState>({}) return (
{/* Email Field */}
{errors.email && ( )}
{/* Password Field */}

Password must be at least 12 characters

{errors.password && ( )}
{/* Submit Button */}
{/* Form-level error */} {errors.form && (
{errors.form}
)}
) } ``` ### 5. Focus Indicators **Custom Focus Styles:** ```css /* frontend/src/index.css */ /* Remove default outline and add custom focus ring */ *:focus { outline: none; } *:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; } /* Button focus styles */ button:focus-visible, a:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; } /* Input focus styles */ input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; border-color: hsl(var(--ring)); } /* Skip to content link */ .skip-to-content:focus { position: absolute; top: 1rem; left: 1rem; z-index: 9999; padding: 0.75rem 1rem; background: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-radius: 0.375rem; } ``` ### 6. Color Contrast **Check and Fix Contrast:** ```typescript // Use colors that meet WCAG AA standards // ❌ Bad: Low contrast (2.5:1)

Low contrast text

// ✅ Good: High contrast (4.5:1+)

High contrast text

// ✅ Good: Using theme colors with proper contrast

Theme colors

// For links, ensure visible distinction Link text ``` **Contrast Checker Function:** ```typescript // frontend/src/utils/colorContrast.ts export const getContrastRatio = (color1: string, color2: string): number => { const getLuminance = (color: string) => { // Convert hex to RGB const rgb = parseInt(color.slice(1), 16) const r = (rgb >> 16) & 0xff const g = (rgb >> 8) & 0xff const b = (rgb >> 0) & 0xff // Calculate relative luminance const [rs, gs, bs] = [r, g, b].map(c => { c = c / 255 return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4) }) return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs } const lum1 = getLuminance(color1) const lum2 = getLuminance(color2) const lighter = Math.max(lum1, lum2) const darker = Math.min(lum1, lum2) return (lighter + 0.05) / (darker + 0.05) } export const meetsWCAGAA = (color1: string, color2: string, isLargeText: boolean = false): boolean => { const contrast = getContrastRatio(color1, color2) return isLargeText ? contrast >= 3 : contrast >= 4.5 } // Usage console.log(meetsWCAGAA('#0066CC', '#FFFFFF')) // true (7.4:1) console.log(meetsWCAGAA('#808080', '#FFFFFF')) // false (3.9:1) ``` ### 7. Images and Alt Text ```typescript // ❌ Bad: Missing alt text // ✅ Good: Descriptive alt text John Doe, Senior QA Engineer // ✅ Decorative images // ✅ Complex images with longer descriptions
Bar chart showing test coverage by module
The chart shows test coverage percentages for each module: Authentication (95%), User Management (88%), Reports (76%), Settings (92%).
``` ### 8. Live Regions for Dynamic Content ```typescript // frontend/src/components/StatusMessage.tsx export const StatusMessage = ({ message, type }: { message: string; type: 'success' | 'error' | 'info' }) => { return (
{message}
) } // Usage ``` ### 9. Accessible Data Tables ```typescript // frontend/src/components/admin/AccessibleTable.tsx export const TeamMembersTable = ({ members }: { members: TeamMember[] }) => { return ( {members.map((member, index) => ( ))}
List of {members.length} team members
Photo Name Role Email Actions
{`${member.name}'s {member.name} {member.role} {member.email}
) } ``` ### 10. Screen Reader Only Text ```css /* frontend/src/index.css */ /* Screen reader only class */ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; } /* Show on focus (for skip links) */ .sr-only:focus { position: static; width: auto; height: auto; padding: initial; margin: initial; overflow: visible; clip: auto; white-space: normal; } ``` ```typescript // Usage Current page ``` ## Accessibility Testing ### 1. Automated Testing with axe-core ```bash cd frontend npm install -D @axe-core/playwright ``` ```python # tests/e2e/test_accessibility.py from axe_playwright_python import Axe def test_homepage_accessibility(page): """Test homepage accessibility.""" page.goto('http://localhost:5173') # Run axe accessibility scan axe = Axe() results = axe.run(page) violations = results['violations'] if violations: print(f"\nFound {len(violations)} accessibility violations:\n") for violation in violations: print(f"❌ {violation['id']}: {violation['description']}") print(f" Impact: {violation['impact']}") print(f" Help: {violation['helpUrl']}") print(f" Affected nodes: {len(violation['nodes'])}\n") # Assert no violations assert len(violations) == 0, f"Found {len(violations)} accessibility violations" def test_admin_login_accessibility(page): """Test login form accessibility.""" page.goto('http://localhost:5173/admin/login') axe = Axe() results = axe.run(page) assert len(results['violations']) == 0 ``` ### 2. Keyboard Navigation Testing ```python # tests/e2e/test_keyboard_navigation.py def test_keyboard_navigation(page): """Test keyboard navigation through the page.""" page.goto('http://localhost:5173') # Start from top page.keyboard.press('Tab') # Should focus skip link first expect(page.locator('.skip-to-content')).to_be_focused() # Tab through navigation page.keyboard.press('Tab') expect(page.locator('nav a:nth-child(1)')).to_be_focused() # Test Enter key activation page.keyboard.press('Enter') # Should navigate def test_modal_focus_trap(page): """Test focus is trapped inside modal.""" page.goto('http://localhost:5173/admin/team-members') # Open modal page.click('button:has-text("Add Team Member")') # Tab through all focusable elements # Last Tab should cycle back to first element for _ in range(10): page.keyboard.press('Tab') # Focus should still be inside modal assert page.locator('[role="dialog"]').evaluate('el => el.contains(document.activeElement)') # Escape should close modal page.keyboard.press('Escape') expect(page.locator('[role="dialog"]')).not_to_be_visible() ``` ### 3. Screen Reader Testing **Test with actual screen readers:** - **macOS:** VoiceOver (Cmd+F5) - **Windows:** NVDA (free), JAWS (paid) - **Linux:** Orca **Test checklist:** - [ ] All images have appropriate alt text - [ ] All form inputs have labels - [ ] Error messages are announced - [ ] Dynamic content changes are announced (aria-live) - [ ] Headings structure is logical - [ ] Landmarks are properly identified (header, nav, main, footer) - [ ] Lists are properly marked up ### 4. Color Contrast Testing ```bash # Install contrast checker npm install -D axe-core # Run contrast check npx axe http://localhost:5173 --rules=color-contrast ``` ## WCAG 2.1 AA Checklist ### Perceivable - [ ] All images have alt text - [ ] Videos have captions (if applicable) - [ ] Color is not the only means of conveying information - [ ] Text contrast >= 4.5:1 (normal), >= 3:1 (large text 18pt+) - [ ] Text can be resized to 200% without loss of content - [ ] Images of text avoided (use real text) ### Operable - [ ] All functionality available via keyboard - [ ] No keyboard trap - [ ] Skip to main content link present - [ ] Page titles are descriptive - [ ] Link purpose clear from link text or context - [ ] Multiple ways to find pages (navigation, search, sitemap) - [ ] Headings and labels are descriptive - [ ] Focus indicator visible - [ ] No time limits (or user can extend) - [ ] No content flashing more than 3 times per second ### Understandable - [ ] Language of page declared (html lang="en") - [ ] Language of parts declared if different - [ ] Navigation is consistent across pages - [ ] Labels or instructions provided for user input - [ ] Error messages are clear and helpful - [ ] Error prevention for important actions (confirmation) - [ ] Form fields have visible labels - [ ] Required fields are indicated ### Robust - [ ] HTML validates (use W3C validator) - [ ] Name, role, value available for all UI components - [ ] Status messages programmatically determinable (aria-live) - [ ] Works with assistive technologies ## Accessibility Resources **Tools:** - **axe DevTools:** Browser extension for accessibility testing - **Lighthouse:** Built into Chrome DevTools - **WAVE:** Web accessibility evaluation tool - **Color Contrast Analyzer:** Check color combinations - **Screen readers:** NVDA (Windows), VoiceOver (macOS), JAWS (Windows) **Documentation:** - WCAG 2.1: https://www.w3.org/WAI/WCAG21/quickref/ - ARIA Authoring Practices: https://www.w3.org/WAI/ARIA/apg/ - MDN Accessibility: https://developer.mozilla.org/en-US/docs/Web/Accessibility ## Report ✅ WCAG 2.1 AA compliance achieved ✅ All images have descriptive alt text ✅ Semantic HTML used throughout ✅ ARIA labels added to interactive elements ✅ Keyboard navigation fully functional ✅ Focus indicators visible and clear ✅ Color contrast meets 4.5:1 minimum ✅ Forms fully accessible with proper labels ✅ Screen reader tested (VoiceOver/NVDA) ✅ Skip to content link implemented ✅ No accessibility violations found (axe-core) ✅ Automated tests passing