$npx -y skills add addyosmani/web-quality-skills --skill accessibilityAudit and improve web accessibility following WCAG 2.2 guidelines. Use when asked to "improve accessibility", "a11y audit", "WCAG compliance", "screen reader support", "keyboard navigation", or "make accessible".
| 1 | # Accessibility (a11y) |
| 2 | |
| 3 | Comprehensive accessibility guidelines based on WCAG 2.2 and Lighthouse accessibility audits. Goal: make content usable by everyone, including people with disabilities. |
| 4 | |
| 5 | ## WCAG Principles: POUR |
| 6 | |
| 7 | | Principle | Description | |
| 8 | |-----------|-------------| |
| 9 | | **P**erceivable | Content can be perceived through different senses | |
| 10 | | **O**perable | Interface can be operated by all users | |
| 11 | | **U**nderstandable | Content and interface are understandable | |
| 12 | | **R**obust | Content works with assistive technologies | |
| 13 | |
| 14 | ## Conformance levels |
| 15 | |
| 16 | | Level | Requirement | Target | |
| 17 | |-------|-------------|--------| |
| 18 | | **A** | Minimum accessibility | Must pass | |
| 19 | | **AA** | Standard compliance | Should pass (legal requirement in many jurisdictions) | |
| 20 | | **AAA** | Enhanced accessibility | Nice to have | |
| 21 | |
| 22 | --- |
| 23 | |
| 24 | ## Perceivable |
| 25 | |
| 26 | ### Text alternatives (1.1) |
| 27 | |
| 28 | **Images require alt text:** |
| 29 | ```html |
| 30 | <!-- ❌ Missing alt --> |
| 31 | <img src="chart.png"> |
| 32 | |
| 33 | <!-- ✅ Descriptive alt --> |
| 34 | <img src="chart.png" alt="Bar chart showing 40% increase in Q3 sales"> |
| 35 | |
| 36 | <!-- ✅ Decorative image (empty alt) --> |
| 37 | <img src="decorative-border.png" alt="" role="presentation"> |
| 38 | |
| 39 | <!-- ✅ Complex image with longer description --> |
| 40 | <figure> |
| 41 | <img src="infographic.png" alt="2024 market trends infographic" |
| 42 | aria-describedby="infographic-desc"> |
| 43 | <figcaption id="infographic-desc"> |
| 44 | <!-- Detailed description --> |
| 45 | </figcaption> |
| 46 | </figure> |
| 47 | ``` |
| 48 | |
| 49 | **Icon buttons need accessible names:** |
| 50 | ```html |
| 51 | <!-- ❌ No accessible name --> |
| 52 | <button><svg><!-- menu icon --></svg></button> |
| 53 | |
| 54 | <!-- ✅ Using aria-label --> |
| 55 | <button aria-label="Open menu"> |
| 56 | <svg aria-hidden="true"><!-- menu icon --></svg> |
| 57 | </button> |
| 58 | |
| 59 | <!-- ✅ Using visually hidden text --> |
| 60 | <button> |
| 61 | <svg aria-hidden="true"><!-- menu icon --></svg> |
| 62 | <span class="visually-hidden">Open menu</span> |
| 63 | </button> |
| 64 | ``` |
| 65 | |
| 66 | **Visually hidden class:** |
| 67 | ```css |
| 68 | .visually-hidden { |
| 69 | position: absolute; |
| 70 | width: 1px; |
| 71 | height: 1px; |
| 72 | padding: 0; |
| 73 | margin: -1px; |
| 74 | overflow: hidden; |
| 75 | clip: rect(0, 0, 0, 0); |
| 76 | white-space: nowrap; |
| 77 | border: 0; |
| 78 | } |
| 79 | ``` |
| 80 | |
| 81 | ### Color contrast (1.4.3, 1.4.6) |
| 82 | |
| 83 | | Text Size | AA minimum | AAA enhanced | |
| 84 | |-----------|------------|--------------| |
| 85 | | Normal text (< 18px / < 14px bold) | 4.5:1 | 7:1 | |
| 86 | | Large text (≥ 18px / ≥ 14px bold) | 3:1 | 4.5:1 | |
| 87 | | UI components & graphics | 3:1 | 3:1 | |
| 88 | |
| 89 | ```css |
| 90 | /* ❌ Low contrast (2.5:1) */ |
| 91 | .low-contrast { |
| 92 | color: #999; |
| 93 | background: #fff; |
| 94 | } |
| 95 | |
| 96 | /* ✅ Sufficient contrast (7:1) */ |
| 97 | .high-contrast { |
| 98 | color: #333; |
| 99 | background: #fff; |
| 100 | } |
| 101 | |
| 102 | /* ✅ Focus states need contrast too (3:1 against background, WCAG 1.4.11) */ |
| 103 | :focus-visible { |
| 104 | outline: 2px solid currentColor; |
| 105 | outline-offset: 2px; |
| 106 | } |
| 107 | ``` |
| 108 | |
| 109 | **Don't rely on color alone:** |
| 110 | ```html |
| 111 | <!-- ❌ Only color indicates error --> |
| 112 | <input class="error-border"> |
| 113 | <style>.error-border { border-color: red; }</style> |
| 114 | |
| 115 | <!-- ✅ Color + icon + text --> |
| 116 | <div class="field-error"> |
| 117 | <input aria-invalid="true" aria-describedby="email-error"> |
| 118 | <span id="email-error" class="error-message"> |
| 119 | <svg aria-hidden="true"><!-- error icon --></svg> |
| 120 | Please enter a valid email address |
| 121 | </span> |
| 122 | </div> |
| 123 | ``` |
| 124 | |
| 125 | ### Media alternatives (1.2) |
| 126 | |
| 127 | ```html |
| 128 | <!-- Video with captions --> |
| 129 | <video controls> |
| 130 | <source src="video.mp4" type="video/mp4"> |
| 131 | <track kind="captions" src="captions.vtt" srclang="en" label="English" default> |
| 132 | <track kind="descriptions" src="descriptions.vtt" srclang="en" label="Descriptions"> |
| 133 | </video> |
| 134 | |
| 135 | <!-- Audio with transcript --> |
| 136 | <audio controls> |
| 137 | <source src="podcast.mp3" type="audio/mp3"> |
| 138 | </audio> |
| 139 | <details> |
| 140 | <summary>Transcript</summary> |
| 141 | <p>Full transcript text...</p> |
| 142 | </details> |
| 143 | ``` |
| 144 | |
| 145 | --- |
| 146 | |
| 147 | ## Operable |
| 148 | |
| 149 | ### Keyboard accessible (2.1) |
| 150 | |
| 151 | **All functionality must be keyboard accessible.** Prefer native interactive elements — `<button>`, `<a href>`, and form controls handle Enter/Space activation, focus, and assistive-tech semantics for free. Only add manual keyboard handling when you cannot use a native element. |
| 152 | |
| 153 | ```html |
| 154 | <!-- ❌ Non-interactive element with click only: not focusable, no keyboard activation --> |
| 155 | <div class="card" onclick="handleAction()">Open</div> |
| 156 | |
| 157 | <!-- ✅ Best: use a native button --> |
| 158 | <button type="button" onclick="handleAction()">Open</button> |
| 159 | ``` |
| 160 | |
| 161 | ```javascript |
| 162 | // ✅ When you MUST use a non-interactive element (e.g. div with role="button"), |
| 163 | // make it focusable AND handle keyboard activation. Do NOT add this to a native |
| 164 | // <button> — Enter/Space already fire click, so you'd double-trigger. |
| 165 | element.setAttribute('role', 'button'); |
| 166 | element.setAttribute('tabindex', '0'); |
| 167 | element.addEventListener('click', handleAction); |
| 168 | element.addEventListener('keydown', (e) => { |
| 169 | if (e.key === 'Enter' || e.key === ' ') { |
| 170 | e.preventDefault(); |
| 171 | handleAc |