--- name: accessibility description: Audit and improve web accessibility following WCAG 2.1 guidelines. Use when asked to "improve accessibility", "a11y audit", "WCAG compliance", "screen reader support", "keyboard navigation", or "make accessible". Do NOT use for SEO (use seo), performance (use core-web-vitals), or comprehensive site audits covering multiple areas (use web-quality-audit). license: MIT metadata: author: web-quality-skills version: '1.0' --- # Accessibility (a11y) Comprehensive accessibility guidelines based on WCAG 2.1 and Lighthouse accessibility audits. Goal: make content usable by everyone, including people with disabilities. ## WCAG Principles: POUR | Principle | Description | | ------------------ | ------------------------------------------------- | | **P**erceivable | Content can be perceived through different senses | | **O**perable | Interface can be operated by all users | | **U**nderstandable | Content and interface are understandable | | **R**obust | Content works with assistive technologies | ## Conformance levels | Level | Requirement | Target | | ------- | ---------------------- | ----------------------------------------------------- | | **A** | Minimum accessibility | Must pass | | **AA** | Standard compliance | Should pass (legal requirement in many jurisdictions) | | **AAA** | Enhanced accessibility | Nice to have | --- ## Perceivable ### Text alternatives (1.1) **Images require alt text:** ```html Bar chart showing 40% increase in Q3 sales
2024 market trends infographic
``` **Icon buttons need accessible names:** ```html ``` **Visually hidden class:** ```css .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } ``` ### Color contrast (1.4.3, 1.4.6) | Text Size | AA minimum | AAA enhanced | | ---------------------------------- | ---------- | ------------ | | Normal text (< 18px / < 14px bold) | 4.5:1 | 7:1 | | Large text (≥ 18px / ≥ 14px bold) | 3:1 | 4.5:1 | | UI components & graphics | 3:1 | 3:1 | ```css /* ❌ Low contrast (2.5:1) */ .low-contrast { color: #999; background: #fff; } /* ✅ Sufficient contrast (7:1) */ .high-contrast { color: #333; background: #fff; } /* ✅ Focus states need contrast too */ :focus-visible { outline: 2px solid #005fcc; outline-offset: 2px; } ``` **Don't rely on color alone:** ```html
Please enter a valid email address
``` ### Media alternatives (1.2) ```html
Transcript

Full transcript text...

``` --- ## Operable ### Keyboard accessible (2.1) **All functionality must be keyboard accessible:** ```javascript // ❌ Only handles click element.addEventListener('click', handleAction) // ✅ Handles both click and keyboard element.addEventListener('click', handleAction) element.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() handleAction() } }) ``` **No keyboard traps:** ```javascript // Modal focus management function openModal(modal) { const focusableElements = modal.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', ) const firstElement = focusableElements[0] const lastElement = focusableElements[focusableElements.length - 1] // Trap focus within modal modal.addEventListener('keydown', (e) => { if (e.key === 'Tab') { if (e.shiftKey && document.activeElement === firstElement) { e.preventDefault() lastElement.focus() } else if (!e.shiftKey && document.activeElement === lastElement) { e.preventDefault() firstElement.focus() } } if (e.key === 'Escape') { closeModal() } }) firstElement.focus() } ``` ### Focus visible (2.4.7) ```css /* ❌ Never remove focus outlines */ *:focus { outline: none; } /* ✅ Use :focus-visible for keyboard-only focus */ :focus { outline: none; } :focus-visible { outline: 2px solid #005fcc; outline-offset: 2px; } /* ✅ Or custom focus styles */ button:focus-visible { box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5); } ``` ### Skip links (2.4.1) ```html
``` ```css .skip-link { position: absolute; top: -40px; left: 0; background: #000; color: #fff; padding: 8px 16px; z-index: 100; } .skip-link:focus { top: 0; } ``` ### Timing (2.2) ```javascript // Allow users to extend time limits function showSessionWarning() { const modal = createModal({ title: 'Session Expiring', content: 'Your session will expire in 2 minutes.', actions: [ { label: 'Extend session', action: extendSession }, { label: 'Log out', action: logout }, ], timeout: 120000, // 2 minutes to respond }) } ``` ### Motion (2.3) ```css /* Respect reduced motion preference */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } ``` --- ## Understandable ### Page language (3.1.1) ```html

The French word for hello is bonjour.

``` ### Consistent navigation (3.2.3) ```html ``` ### Form labels (3.3.2) ```html

Must be at least 8 characters with one number.

``` ### Error handling (3.3.1, 3.3.3) ```html
``` ```javascript // Focus first error on submit form.addEventListener('submit', (e) => { const firstError = form.querySelector('[aria-invalid="true"]') if (firstError) { e.preventDefault() firstError.focus() // Announce error summary const errorSummary = document.getElementById('error-summary') errorSummary.textContent = `${errors.length} errors found. Please fix them and try again.` errorSummary.focus() } }) ``` --- ## Robust ### Valid HTML (4.1.1) ```html
...
...
...
Click ``` ### ARIA usage (4.1.2) **Prefer native elements:** ```html
Click me
Option
``` **When ARIA is needed:** ```html
``` ### Live regions (4.1.3) ```html
``` ```javascript // Announce dynamic content changes function showNotification(message, type = 'polite') { const container = document.getElementById(`${type}-announcer`) container.textContent = '' // Clear first requestAnimationFrame(() => { container.textContent = message }) } ``` --- ## Testing checklist ### Automated testing ```bash # Lighthouse accessibility audit npx lighthouse https://example.com --only-categories=accessibility # axe-core npm install @axe-core/cli -g axe https://example.com ``` ### Manual testing - [ ] **Keyboard navigation:** Tab through entire page, use Enter/Space to activate - [ ] **Screen reader:** Test with VoiceOver (Mac), NVDA (Windows), or TalkBack (Android) - [ ] **Zoom:** Content usable at 200% zoom - [ ] **High contrast:** Test with Windows High Contrast Mode - [ ] **Reduced motion:** Test with `prefers-reduced-motion: reduce` - [ ] **Focus order:** Logical and follows visual order ### Screen reader commands | Action | VoiceOver (Mac) | NVDA (Windows) | | ------------- | ------------------- | -------------- | | Start/Stop | ⌘ + F5 | Ctrl + Alt + N | | Next item | VO + → | ↓ | | Previous item | VO + ← | ↑ | | Activate | VO + Space | Enter | | Headings list | VO + U, then arrows | H / Shift + H | | Links list | VO + U | K / Shift + K | --- ## Common issues by impact ### Critical (fix immediately) 1. Missing form labels 2. Missing image alt text 3. Insufficient color contrast 4. Keyboard traps 5. No focus indicators ### Serious (fix before launch) 1. Missing page language 2. Missing heading structure 3. Non-descriptive link text 4. Auto-playing media 5. Missing skip links ### Moderate (fix soon) 1. Missing ARIA labels on icons 2. Inconsistent navigation 3. Missing error identification 4. Timing without controls 5. Missing landmark regions ## References - [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/) - [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/) - [Deque axe Rules](https://dequeuniversity.com/rules/axe/) - [Web Quality Audit](../web-quality-audit/SKILL.md)