@diegovelasquezweb/a11y-engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,944 @@
1
+ [
2
+ {
3
+ "criterion": "2.4.11",
4
+ "title": "Focus Appearance",
5
+ "level": "AA",
6
+ "description": "Operational elements must have a visible focus indicator with sufficient contrast and size when focused via keyboard.",
7
+ "steps": [
8
+ "Navigate through the page using the `TAB` key.",
9
+ "Ensure every clickable element (links, buttons, inputs) shows a clear border or background change when focused.",
10
+ "Verify the contrast of the focus indicator is at least 3:1 against the surrounding background."
11
+ ],
12
+ "remediation": [
13
+ "If no focus style is defined globally, add: `*:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }` to the global stylesheet."
14
+ ],
15
+ "code_example": {
16
+ "lang": "css",
17
+ "before": "/* BEFORE — browser default outline suppressed */\n*:focus {\n outline: none;\n}",
18
+ "after": "/* AFTER — accessible focus indicator */\n*:focus-visible {\n outline: 3px solid #005FCC;\n outline-offset: 2px;\n border-radius: 2px;\n}\n/* Only suppress for mouse/pointer interactions */\n*:focus:not(:focus-visible) {\n outline: none;\n}"
19
+ },
20
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance.html"
21
+ },
22
+ {
23
+ "criterion": "2.5.7",
24
+ "title": "Dragging Movements",
25
+ "level": "AA",
26
+ "description": "All drag-and-drop functionality must have a single-pointer alternative (click-to-select + click-to-drop, arrow buttons, etc.).",
27
+ "steps": [
28
+ "Identify components that use drag gestures (e.g., sortable lists, sliders).",
29
+ "Try to perform the same action using only a keyboard or single mouse clicks.",
30
+ "Ensure there is a button or menu-based alternative to complete the task."
31
+ ],
32
+ "remediation": [
33
+ "If no keyboard alternative exists, add arrow-key support or button controls to the component."
34
+ ],
35
+ "code_example": {
36
+ "lang": "html",
37
+ "before": "<!-- BEFORE — drag-only, no keyboard or single-pointer alternative -->\n<ul id=\"sortable\">\n <li draggable=\"true\" ondragstart=\"drag(event)\">Item 1</li>\n <li draggable=\"true\" ondragstart=\"drag(event)\">Item 2</li>\n</ul>",
38
+ "after": "<!-- AFTER — drag + button alternative for keyboard/single-pointer access -->\n<ul id=\"sortable\">\n <li draggable=\"true\" ondragstart=\"drag(event)\">\n <span>Item 1</span>\n <button type=\"button\" aria-label=\"Move Item 1 up\" onclick=\"moveUp(this)\">↑</button>\n <button type=\"button\" aria-label=\"Move Item 1 down\" onclick=\"moveDown(this)\">↓</button>\n </li>\n</ul>"
39
+ },
40
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html"
41
+ },
42
+ {
43
+ "criterion": "2.5.8",
44
+ "title": "Target Size (Minimum)",
45
+ "level": "AA",
46
+ "description": "Interactive targets must be at least 24×24 CSS pixels, or have sufficient spacing to offset a smaller size.",
47
+ "steps": [
48
+ "Inspect small buttons, checkboxes, and inline links.",
49
+ "Measure the clickable area (including padding).",
50
+ "Ensure no interactive element is smaller than 24x24px unless it is an inline link within a paragraph."
51
+ ],
52
+ "remediation": [
53
+ "For icon-only buttons (no text), ensure `padding` is set so the total rendered size is at least 24×24px."
54
+ ],
55
+ "code_example": {
56
+ "lang": "css",
57
+ "before": "/* BEFORE — icon button renders below 24×24 CSS px */\n.icon-btn {\n width: 16px;\n height: 16px;\n padding: 0;\n}",
58
+ "after": "/* AFTER — minimum 24×24 CSS px via padding */\n.icon-btn {\n min-width: 24px;\n min-height: 24px;\n padding: 4px;\n box-sizing: content-box;\n}"
59
+ },
60
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html"
61
+ },
62
+ {
63
+ "criterion": "3.2.6",
64
+ "title": "Consistent Help",
65
+ "level": "A",
66
+ "description": "Help mechanisms (support link, chat widget, contact info) must appear in the same relative order on every page where they appear.",
67
+ "steps": [
68
+ "Find the 'Support' or 'Help' links in the header/footer.",
69
+ "Navigate between at least 3 pages.",
70
+ "Confirm these elements reside in the same layout position and visual sequence."
71
+ ],
72
+ "remediation": [
73
+ "If found on individual pages rather than in a shared layout, move the component to the shared layout to guarantee consistent placement."
74
+ ],
75
+ "code_example": {
76
+ "lang": "html",
77
+ "before": "<!-- BEFORE — help link embedded per-page, missing on some pages -->\n<!-- about.html -->\n<footer><a href=\"/contact\">Contact Support</a></footer>\n<!-- pricing.html: no footer, help link absent -->",
78
+ "after": "<!-- AFTER — help link in shared layout, consistent on all pages -->\n<!-- layout.html (shared template) -->\n<footer>\n <a href=\"/contact\">Contact Support</a>\n</footer>"
79
+ },
80
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/consistent-help.html"
81
+ },
82
+ {
83
+ "criterion": "3.3.7",
84
+ "title": "Redundant Entry",
85
+ "level": "A",
86
+ "description": "Information already provided by the user in the same session must not be required again unless essential or for security.",
87
+ "steps": [
88
+ "Fill out a multi-step form (e.g., Checkout).",
89
+ "In later steps, check if you are asked for information you already entered (e.g., 'Confirm Email').",
90
+ "Ensure this information is either auto-filled or available for selection."
91
+ ],
92
+ "remediation": [
93
+ "If later steps re-render fields already collected (name, address, email), pre-populate them from stored state/context instead of showing empty inputs."
94
+ ],
95
+ "code_example": {
96
+ "lang": "html",
97
+ "before": "<!-- BEFORE — step 2 re-asks for email already submitted in step 1 -->\n<label for=\"email2\">Re-enter your email</label>\n<input type=\"email\" id=\"email2\" name=\"email_confirm\">",
98
+ "after": "<!-- AFTER — email pre-populated from stored session state -->\n<label for=\"email2\">Email (entered in step 1)</label>\n<input\n type=\"email\"\n id=\"email2\"\n name=\"email_confirm\"\n value=\"user@example.com\"\n readonly\n aria-describedby=\"email2-hint\"\n>\n<p id=\"email2-hint\">Wrong address? <a href=\"/step-1\">Go back to edit it.</a></p>"
99
+ },
100
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/redundant-entry.html"
101
+ },
102
+ {
103
+ "criterion": "3.3.8",
104
+ "title": "Accessible Authentication (Minimum)",
105
+ "level": "AA",
106
+ "description": "Authentication processes (login) must not rely on cognitive function tests (memorizing complex passwords, solving puzzles) unless an alternative exists.",
107
+ "steps": [
108
+ "Visit the Login page.",
109
+ "Check if you can use a password manager (Auto-fill).",
110
+ "Check if you can paste into the password field.",
111
+ "Verify no 'solve this math problem' type CAPTCHAs are mandatory."
112
+ ],
113
+ "remediation": [
114
+ "Verify password fields do not have `onPaste` handlers that prevent pasting — remove any such restriction."
115
+ ],
116
+ "code_example": {
117
+ "lang": "html",
118
+ "before": "<!-- BEFORE — blocks paste and autocomplete -->\n<input\n type=\"password\"\n name=\"password\"\n autocomplete=\"off\"\n onpaste=\"return false;\"\n>",
119
+ "after": "<!-- AFTER — allows password manager and paste -->\n<input\n type=\"password\"\n name=\"password\"\n autocomplete=\"current-password\"\n>"
120
+ },
121
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/accessible-authentication-minimum.html"
122
+ },
123
+ {
124
+ "criterion": "2.1.1",
125
+ "title": "Keyboard Access",
126
+ "level": "A",
127
+ "description": "All functionality must be operable through a keyboard interface without requiring specific timings for individual keystrokes.",
128
+ "steps": [
129
+ "Unplug or disable your mouse and navigate the entire page using only Tab, Shift+Tab, Enter, Space, and arrow keys.",
130
+ "Confirm every interactive feature (menus, modals, carousels, date pickers) can be opened, used, and closed by keyboard.",
131
+ "Flag any action that requires hover or mouse click with no keyboard equivalent."
132
+ ],
133
+ "remediation": [
134
+ "Add keyboard event handlers (keydown/keyup) alongside mouse event handlers for all custom interactive components.",
135
+ "Ensure custom components follow the APG keyboard interaction patterns: https://www.w3.org/WAI/ARIA/apg/patterns/"
136
+ ],
137
+ "code_example": {
138
+ "lang": "html",
139
+ "before": "<!-- BEFORE — click only, no keyboard support -->\n<div class=\"dropdown\" onclick=\"toggleMenu()\">\n Options\n</div>",
140
+ "after": "<!-- AFTER — keyboard accessible with role + tabindex -->\n<button\n class=\"dropdown\"\n aria-expanded=\"false\"\n aria-haspopup=\"menu\"\n onclick=\"toggleMenu(this)\"\n onkeydown=\"handleMenuKey(event, this)\"\n>\n Options\n</button>"
141
+ },
142
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html"
143
+ },
144
+ {
145
+ "criterion": "2.1.2",
146
+ "title": "No Keyboard Trap",
147
+ "level": "A",
148
+ "description": "Keyboard focus must never become locked inside a component unless it is a modal dialog, and even then Escape must close it.",
149
+ "steps": [
150
+ "Tab into every interactive component (menus, widgets, embedded media).",
151
+ "Attempt to Tab or Shift+Tab out of each component — confirm focus moves to the next element.",
152
+ "For modal dialogs, press Escape and confirm the dialog closes and focus returns to the trigger."
153
+ ],
154
+ "remediation": [
155
+ "For modals, implement a focus trap that keeps focus inside while open, and releases it when closed with Escape or the close button.",
156
+ "Remove any tabindex or focus logic that prevents Tab from exiting a non-modal component."
157
+ ],
158
+ "code_example": {
159
+ "lang": "js",
160
+ "before": "// BEFORE — widget traps all Tab keypresses, focus cannot exit\nwidget.addEventListener('keydown', (e) => {\n if (e.key === 'Tab') {\n e.preventDefault(); // blocks exit\n cycleFocusInternally(e.shiftKey);\n }\n});",
161
+ "after": "// AFTER — focus trap only inside open modal dialogs\nwidget.addEventListener('keydown', (e) => {\n const isModal = widget.getAttribute('role') === 'dialog';\n if (e.key === 'Tab' && isModal && isOpen) {\n trapFocusWithin(widget, e);\n }\n // Non-modal: let Tab exit naturally\n});"
162
+ },
163
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/no-keyboard-trap.html"
164
+ },
165
+ {
166
+ "criterion": "2.4.3",
167
+ "title": "Focus Order",
168
+ "level": "A",
169
+ "description": "The keyboard focus sequence must follow a logical order that preserves meaning and operability.",
170
+ "steps": [
171
+ "Press Tab repeatedly from the top of the page and observe the focus sequence.",
172
+ "Confirm focus moves left-to-right and top-to-bottom (or in the logical reading order of the content).",
173
+ "Check that modals and dynamic content (drawers, toasts) receive focus in the correct sequence when they appear."
174
+ ],
175
+ "remediation": [
176
+ "Remove positive tabindex values — they override the natural DOM order and create unpredictable sequences.",
177
+ "Reorder DOM elements to match the intended visual and logical reading order."
178
+ ],
179
+ "code_example": {
180
+ "lang": "html",
181
+ "before": "<!-- BEFORE — positive tabindex creates unnatural focus order -->\n<button tabindex=\"3\">Submit</button>\n<input tabindex=\"1\" type=\"text\" placeholder=\"First name\">\n<input tabindex=\"2\" type=\"text\" placeholder=\"Last name\">",
182
+ "after": "<!-- AFTER — natural DOM order, no positive tabindex -->\n<input type=\"text\" placeholder=\"First name\">\n<input type=\"text\" placeholder=\"Last name\">\n<button type=\"submit\">Submit</button>"
183
+ },
184
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html"
185
+ },
186
+ {
187
+ "criterion": "1.4.1",
188
+ "title": "Use of Color",
189
+ "level": "A",
190
+ "description": "Color must not be the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element.",
191
+ "steps": [
192
+ "Enable grayscale mode (DevTools → Rendering → Emulate vision deficiencies: Achromatopsia).",
193
+ "Check that all meaningful information is still understandable without color (e.g. error states, required fields, status indicators).",
194
+ "Confirm links within body text are distinguishable from surrounding text by more than color alone (e.g. underline)."
195
+ ],
196
+ "remediation": [
197
+ "Add a secondary indicator alongside color: icon, pattern, label, underline, or border.",
198
+ "Never rely solely on red/green color coding for status — add text labels or icons."
199
+ ],
200
+ "code_example": {
201
+ "lang": "html",
202
+ "before": "<!-- BEFORE — error indicated by color only -->\n<input type=\"email\" class=\"input-error\">\n<!-- .input-error { border: 2px solid red; } -->",
203
+ "after": "<!-- AFTER — error indicated by color + icon + text -->\n<div class=\"field\">\n <input type=\"email\" class=\"input-error\" aria-describedby=\"email-error\">\n <p id=\"email-error\" class=\"error-message\" role=\"alert\">\n <svg aria-hidden=\"true\"><!-- error icon --></svg>\n Please enter a valid email address.\n </p>\n</div>"
204
+ },
205
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html"
206
+ },
207
+ {
208
+ "criterion": "1.4.10",
209
+ "title": "Reflow",
210
+ "level": "AA",
211
+ "description": "Content must reflow into a single column at 320 CSS pixels wide without requiring horizontal scrolling.",
212
+ "steps": [
213
+ "In DevTools, set the viewport to 320px wide (iPhone SE size or use the responsive mode).",
214
+ "Scroll vertically through the entire page — confirm no content requires horizontal scrolling to read.",
215
+ "Check that no text is clipped, truncated, or overlapping at this width."
216
+ ],
217
+ "remediation": [
218
+ "Use relative units (rem, %, vw) instead of fixed pixel widths for layout containers.",
219
+ "Add a CSS media query at max-width: 320px to collapse multi-column layouts into a single column."
220
+ ],
221
+ "code_example": {
222
+ "lang": "css",
223
+ "before": "/* BEFORE — fixed-width layout, causes horizontal scroll at 320px */\n.container {\n width: 960px;\n}\n.sidebar {\n width: 300px;\n float: left;\n}",
224
+ "after": "/* AFTER — responsive layout, single column at 320px */\n.container {\n width: 100%;\n max-width: 960px;\n padding: 0 1rem;\n box-sizing: border-box;\n}\n.sidebar { width: 100%; }\n@media (min-width: 640px) {\n .sidebar {\n width: 30%;\n float: left;\n }\n}"
225
+ },
226
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/reflow.html"
227
+ },
228
+ {
229
+ "criterion": "1.4.11",
230
+ "title": "Non-text Contrast",
231
+ "level": "AA",
232
+ "description": "UI components (buttons, inputs, icons) and informational graphics must have a contrast ratio of at least 3:1 against adjacent colors.",
233
+ "steps": [
234
+ "Use the DevTools color picker to sample the border color of form inputs and compare it against the background.",
235
+ "Check icon-only buttons — the icon itself must contrast at least 3:1 against its background.",
236
+ "Inspect focus indicators — the outline color must contrast at least 3:1 against the surrounding area."
237
+ ],
238
+ "remediation": [
239
+ "Darken input borders, icon strokes, and UI component outlines to meet the 3:1 ratio.",
240
+ "Use the WebAIM Contrast Checker or Colour Contrast Analyser to validate values."
241
+ ],
242
+ "code_example": {
243
+ "lang": "css",
244
+ "before": "/* BEFORE — input border fails 3:1 against white background */\ninput[type=\"text\"] {\n border: 1px solid #ccc; /* ~1.6:1 contrast */\n}\n.icon { fill: #bbb; } /* ~1.4:1 contrast */",
245
+ "after": "/* AFTER — border and icon meet 3:1 minimum */\ninput[type=\"text\"] {\n border: 1px solid #767676; /* ~4.5:1 against white */\n}\n.icon { fill: #595959; } /* ~7:1 against white */"
246
+ },
247
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html"
248
+ },
249
+ {
250
+ "criterion": "1.4.12",
251
+ "title": "Text Spacing",
252
+ "level": "AA",
253
+ "description": "No content or functionality must be lost when users apply custom text spacing: line-height 1.5, letter-spacing 0.12em, word-spacing 0.16em, paragraph spacing 2em.",
254
+ "steps": [
255
+ "Install the Text Spacing bookmarklet (Steve Faulkner / TPGi) or paste the following CSS into DevTools: `* { line-height: 1.5 !important; letter-spacing: 0.12em !important; word-spacing: 0.16em !important; } p { margin-bottom: 2em !important; }`",
256
+ "Scroll through the page and confirm no text is clipped, overlapping, or hidden.",
257
+ "Check form labels, buttons, and navigation items specifically — these commonly overflow."
258
+ ],
259
+ "remediation": [
260
+ "Use min-height instead of fixed height on containers with text.",
261
+ "Avoid overflow:hidden on elements that contain user-readable text."
262
+ ],
263
+ "code_example": {
264
+ "lang": "css",
265
+ "before": "/* BEFORE — fixed height clips text with custom user spacing */\n.card-header {\n height: 48px;\n overflow: hidden;\n}\n.btn {\n height: 40px;\n line-height: 40px;\n}",
266
+ "after": "/* AFTER — min-height allows text to grow with user spacing overrides */\n.card-header {\n min-height: 48px;\n overflow: visible;\n}\n.btn {\n min-height: 40px;\n padding: 0.5em 1em;\n line-height: 1.5;\n}"
267
+ },
268
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/text-spacing.html"
269
+ },
270
+ {
271
+ "criterion": "1.4.13",
272
+ "title": "Content on Hover or Focus",
273
+ "level": "AA",
274
+ "description": "Additional content that appears on hover or focus (tooltips, sub-menus) must be dismissable, hoverable, and persistent.",
275
+ "steps": [
276
+ "Hover over elements that trigger tooltips or popovers — confirm the content stays visible while you move the cursor onto it.",
277
+ "Press Escape while a tooltip is visible — confirm it dismisses without moving focus.",
278
+ "Trigger the same content via keyboard focus (Tab) — confirm it appears and remains visible until focus moves away."
279
+ ],
280
+ "remediation": [
281
+ "Ensure tooltip/popover elements are adjacent in DOM so the cursor can move from trigger to content without dismissal.",
282
+ "Add an Escape key handler to close hover content without requiring focus change."
283
+ ],
284
+ "code_example": {
285
+ "lang": "html",
286
+ "before": "<!-- BEFORE — tooltip closes when cursor moves onto it (pointer-events: none) -->\n<span class=\"tip-trigger\">Help <span class=\"tooltip\">More info.</span></span>\n<!-- CSS: .tooltip { pointer-events: none; } -->",
287
+ "after": "<!-- AFTER — tooltip is hoverable, keyboard-focusable, Escape dismisses -->\n<span class=\"tip-trigger\">\n <button type=\"button\" aria-describedby=\"tip1\">Help</button>\n <span id=\"tip1\" role=\"tooltip\" class=\"tooltip\">More info.</span>\n</span>\n<!-- CSS: .tooltip { pointer-events: auto; } -->\n<!-- JS: document.addEventListener('keydown', e => { if (e.key === 'Escape') hideTooltip(); }); -->"
288
+ },
289
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html"
290
+ },
291
+ {
292
+ "criterion": "3.3.1",
293
+ "title": "Error Identification",
294
+ "level": "A",
295
+ "description": "When a form input error is detected, the item in error must be identified and the error described to the user in text.",
296
+ "steps": [
297
+ "Submit a form with intentionally invalid or empty required fields.",
298
+ "Confirm each error is identified by text — not only by a red border or icon.",
299
+ "Verify the error message describes what is wrong (e.g. 'Email is required' not just 'Invalid input').",
300
+ "Check that focus moves to the first error field or an error summary at the top of the form."
301
+ ],
302
+ "remediation": [
303
+ "Add aria-describedby on the input pointing to the error message element.",
304
+ "Use aria-invalid=\"true\" on inputs in an error state.",
305
+ "Move focus to the error summary or first error field on form submission failure."
306
+ ],
307
+ "code_example": {
308
+ "lang": "html",
309
+ "before": "<!-- BEFORE — error indicated only by red border, no text description -->\n<input type=\"email\" name=\"email\" class=\"input-error\">",
310
+ "after": "<!-- AFTER — error described in text, linked via aria-describedby -->\n<label for=\"email\">Email address</label>\n<input\n type=\"email\"\n id=\"email\"\n name=\"email\"\n aria-invalid=\"true\"\n aria-describedby=\"email-err\"\n class=\"input-error\"\n>\n<p id=\"email-err\" class=\"error-msg\">Please enter a valid email address.</p>"
311
+ },
312
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html"
313
+ },
314
+ {
315
+ "criterion": "AT-1",
316
+ "title": "Screen Reader: Heading Navigation",
317
+ "level": "AT",
318
+ "description": "A screen reader user must be able to navigate the page efficiently using heading shortcuts.",
319
+ "steps": [
320
+ "Open VoiceOver (macOS: Cmd+F5) or NVDA (Windows: Ctrl+Alt+N).",
321
+ "Use the heading navigation shortcut (VoiceOver: VO+Cmd+H — NVDA: H) to jump between headings.",
322
+ "Confirm all major sections are reachable by heading and announced with the correct level (e.g. 'Heading level 2').",
323
+ "Confirm no decorative or off-screen elements are announced as headings."
324
+ ],
325
+ "remediation": [
326
+ "Ensure visual headings use semantic <h1>–<h6> elements, not styled <div> or <span> elements.",
327
+ "Remove role=\"heading\" from decorative elements."
328
+ ],
329
+ "code_example": {
330
+ "lang": "html",
331
+ "before": "<!-- BEFORE — visual heading styled as div, not announced by screen reader -->\n<div class=\"section-title\">Product Features</div>",
332
+ "after": "<!-- AFTER — semantic heading, announced as 'Heading level 2, Product Features' -->\n<h2>Product Features</h2>"
333
+ },
334
+ "ref": "https://www.w3.org/WAI/test-evaluate/preliminary/#headings"
335
+ },
336
+ {
337
+ "criterion": "AT-2",
338
+ "title": "Screen Reader: Landmark Navigation",
339
+ "level": "AT",
340
+ "description": "A screen reader user must be able to navigate between page regions using landmark shortcuts.",
341
+ "steps": [
342
+ "Open VoiceOver or NVDA.",
343
+ "Use the landmark navigation shortcut (VoiceOver: VO+Cmd+W — NVDA: D) to cycle through landmarks.",
344
+ "Confirm main, nav, header, footer, and any aside regions are announced with descriptive labels.",
345
+ "Confirm no orphan content (text outside any landmark) is present."
346
+ ],
347
+ "remediation": [
348
+ "Add aria-label to disambiguate multiple nav or section elements (e.g. aria-label=\"Main navigation\").",
349
+ "Wrap all visible page content in semantic landmark elements."
350
+ ],
351
+ "code_example": {
352
+ "lang": "html",
353
+ "before": "<!-- BEFORE — layout divs only, no landmark regions for screen reader navigation -->\n<div class=\"nav-bar\">...</div>\n<div class=\"content\">...</div>\n<div class=\"side\">...</div>",
354
+ "after": "<!-- AFTER — semantic landmarks, duplicate navs labeled for disambiguation -->\n<nav aria-label=\"Main navigation\">...</nav>\n<main>...</main>\n<aside aria-label=\"Related articles\">...</aside>"
355
+ },
356
+ "ref": "https://www.w3.org/WAI/test-evaluate/preliminary/#landmarks"
357
+ },
358
+ {
359
+ "criterion": "AT-3",
360
+ "title": "Screen Reader: Form Labels",
361
+ "level": "AT",
362
+ "description": "Every form field must be announced with its label and type when focused by a screen reader.",
363
+ "steps": [
364
+ "Open VoiceOver or NVDA and Tab into each form field.",
365
+ "Confirm the screen reader announces the field label, input type, and any required state (e.g. 'Email, required, edit text').",
366
+ "Confirm placeholder text alone is NOT the only label — placeholders disappear on input and are not reliably announced."
367
+ ],
368
+ "remediation": [
369
+ "Associate every input with a <label> element using for/id, or use aria-label / aria-labelledby.",
370
+ "Never rely on placeholder as a substitute for a visible label."
371
+ ],
372
+ "code_example": {
373
+ "lang": "html",
374
+ "before": "<!-- BEFORE — placeholder is the only label (unreliable, disappears on input) -->\n<input type=\"email\" placeholder=\"Email address\">",
375
+ "after": "<!-- AFTER — explicit visible label associated via for/id -->\n<label for=\"email\">Email address</label>\n<input type=\"email\" id=\"email\" placeholder=\"e.g. name@example.com\">"
376
+ },
377
+ "ref": "https://www.w3.org/WAI/tutorials/forms/labels/"
378
+ },
379
+ {
380
+ "criterion": "AT-4",
381
+ "title": "Screen Reader: Interactive Element Activation",
382
+ "level": "AT",
383
+ "description": "All interactive elements must be operable and correctly announced when activated via keyboard with a screen reader running.",
384
+ "steps": [
385
+ "With VoiceOver or NVDA active, Tab to buttons, links, and custom widgets.",
386
+ "Activate each element using Enter (links, buttons) or Space (buttons, checkboxes).",
387
+ "Confirm the resulting action is announced (e.g. dialog opens, content expands, page navigates).",
388
+ "Confirm custom components (role=\"button\", role=\"checkbox\") respond to the same keys as native equivalents."
389
+ ],
390
+ "remediation": [
391
+ "Add keydown handlers for Enter and Space on all elements with role=\"button\".",
392
+ "Ensure state changes trigger an accessible announcement via aria-live or focus management."
393
+ ],
394
+ "code_example": {
395
+ "lang": "html",
396
+ "before": "<!-- BEFORE — div with onclick, not keyboard-operable, announced as generic element -->\n<div class=\"btn\" onclick=\"submitForm()\">Submit</div>",
397
+ "after": "<!-- AFTER — native button, keyboard-operable, announced as 'Submit, button' -->\n<button type=\"button\" onclick=\"submitForm()\">Submit</button>"
398
+ },
399
+ "ref": "https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/"
400
+ },
401
+ {
402
+ "criterion": "AT-5",
403
+ "title": "Screen Reader: Live Region Announcements",
404
+ "level": "AT",
405
+ "description": "Dynamic content updates (alerts, status messages, loading states) must be announced by the screen reader without requiring focus.",
406
+ "steps": [
407
+ "With VoiceOver or NVDA active, trigger dynamic content: submit a form, load more results, trigger a toast/notification.",
408
+ "Confirm the screen reader announces the update automatically without the user moving focus.",
409
+ "Confirm loading spinners are announced as 'Loading' and completion is announced when done."
410
+ ],
411
+ "remediation": [
412
+ "Add aria-live=\"polite\" for non-urgent updates (search results, status messages).",
413
+ "Add aria-live=\"assertive\" only for urgent alerts that must interrupt the user.",
414
+ "Use role=\"status\" for polite messages and role=\"alert\" for urgent ones."
415
+ ],
416
+ "code_example": {
417
+ "lang": "html",
418
+ "before": "<!-- BEFORE — status updated silently, screen reader not notified -->\n<div id=\"status\"></div>\n<script>\n document.getElementById('status').textContent = '12 results found';\n</script>",
419
+ "after": "<!-- AFTER — aria-live announces update without requiring focus change -->\n<div id=\"status\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n<script>\n // Screen reader announces '12 results found' automatically\n document.getElementById('status').textContent = '12 results found';\n</script>"
420
+ },
421
+ "ref": "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides/Live_regions"
422
+ },
423
+ {
424
+ "criterion": "AT-6",
425
+ "title": "Screen Reader: Modal Dialog Behavior",
426
+ "level": "AT",
427
+ "description": "Modal dialogs must trap focus, announce their role and label, and return focus to the trigger on close.",
428
+ "steps": [
429
+ "Open VoiceOver or NVDA and trigger a modal dialog.",
430
+ "Confirm the screen reader announces 'dialog' and the dialog's title when it opens.",
431
+ "Press Tab repeatedly — confirm focus cycles only within the modal and does not escape to the background.",
432
+ "Close the dialog with Escape or the close button — confirm focus returns to the element that triggered it."
433
+ ],
434
+ "remediation": [
435
+ "Add role=\"dialog\" and aria-modal=\"true\" to the dialog container.",
436
+ "Add aria-labelledby pointing to the dialog title.",
437
+ "Implement a JavaScript focus trap and restore focus on close."
438
+ ],
439
+ "code_example": {
440
+ "lang": "html",
441
+ "before": "<!-- BEFORE — visual modal, no ARIA, focus not managed on open -->\n<div class=\"modal\">\n <h2>Confirm</h2>\n <button onclick=\"close()\">Cancel</button>\n <button onclick=\"confirm()\">OK</button>\n</div>",
442
+ "after": "<!-- AFTER — accessible dialog with focus management and Escape support -->\n<div\n class=\"modal\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"modal-title\"\n tabindex=\"-1\"\n>\n <h2 id=\"modal-title\">Confirm</h2>\n <button onclick=\"close()\">Cancel</button>\n <button onclick=\"confirm()\">OK</button>\n</div>\n<!-- JS: focus dialog on open, trap Tab within, restore focus to trigger on close -->"
443
+ },
444
+ "ref": "https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/"
445
+ },
446
+ {
447
+ "criterion": "AT-7",
448
+ "title": "Screen Reader: Table Reading",
449
+ "level": "AT",
450
+ "description": "Data tables must be navigable by a screen reader with row and column headers announced for each cell.",
451
+ "steps": [
452
+ "With VoiceOver or NVDA, navigate into a data table using the table navigation shortcut (NVDA: T).",
453
+ "Move between cells using arrow keys — confirm the screen reader announces the column and row header for each cell.",
454
+ "Confirm the table has an accessible caption or aria-label describing its purpose."
455
+ ],
456
+ "remediation": [
457
+ "Use <th> with scope=\"col\" or scope=\"row\" for all header cells.",
458
+ "Add a <caption> or aria-label to the <table> element.",
459
+ "Never use tables for layout — only for tabular data."
460
+ ],
461
+ "code_example": {
462
+ "lang": "html",
463
+ "before": "<!-- BEFORE — table without headers or caption, cells read in isolation -->\n<table>\n <tr><td>Name</td><td>Role</td></tr>\n <tr><td>Alice</td><td>Engineer</td></tr>\n</table>",
464
+ "after": "<!-- AFTER — scoped headers and caption for full screen reader context -->\n<table>\n <caption>Team Members</caption>\n <thead>\n <tr>\n <th scope=\"col\">Name</th>\n <th scope=\"col\">Role</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td>Alice</td>\n <td>Engineer</td>\n </tr>\n </tbody>\n</table>"
465
+ },
466
+ "ref": "https://www.w3.org/WAI/tutorials/tables/"
467
+ },
468
+ {
469
+ "criterion": "AT-8",
470
+ "title": "Screen Reader: Form Error Announcement",
471
+ "level": "AT",
472
+ "description": "Form validation errors must be announced by the screen reader so users know what failed and why.",
473
+ "steps": [
474
+ "With VoiceOver or NVDA active, submit a form with intentionally invalid data.",
475
+ "Confirm the screen reader announces the error(s) — either via focus moving to an error summary or via an aria-live region.",
476
+ "Tab to each invalid field and confirm the error message is announced alongside the field label.",
477
+ "Confirm aria-invalid=\"true\" is set on fields in error state."
478
+ ],
479
+ "remediation": [
480
+ "On validation failure, move focus to an error summary at the top of the form with role=\"alert\".",
481
+ "Set aria-invalid=\"true\" and aria-describedby on each invalid input pointing to its error message.",
482
+ "Ensure error messages are visible text nodes — not only communicated via color or icon."
483
+ ],
484
+ "code_example": {
485
+ "lang": "html",
486
+ "before": "<!-- BEFORE — error displayed visually, screen reader not notified -->\n<input type=\"email\" class=\"invalid\">\n<div class=\"error\">Email is required</div>",
487
+ "after": "<!-- AFTER — error summary announced via role=alert, field marked invalid -->\n<div role=\"alert\" id=\"error-summary\">\n <p>1 error: <a href=\"#email\">Email is required</a></p>\n</div>\n<label for=\"email\">Email</label>\n<input\n type=\"email\"\n id=\"email\"\n aria-invalid=\"true\"\n aria-describedby=\"email-err\"\n class=\"invalid\"\n>\n<p id=\"email-err\">Email is required.</p>"
488
+ },
489
+ "ref": "https://www.w3.org/WAI/tutorials/forms/validation/"
490
+ },
491
+ {
492
+ "criterion": "1.2.1",
493
+ "title": "Audio-only and Video-only (Prerecorded)",
494
+ "level": "A",
495
+ "description": "Audio-only content needs a text transcript. Video-only content needs a text or audio description of the visual information.",
496
+ "steps": [
497
+ "Find all <audio> and <video> elements on the page.",
498
+ "For audio-only content: check for a link to a text transcript near the player.",
499
+ "For video-only content (no audio track): check for a text description or an audio description track."
500
+ ],
501
+ "remediation": [
502
+ "Add a linked transcript below or near audio-only players.",
503
+ "For video-only content, add a <track kind=\"descriptions\"> element or provide a written description adjacent to the player."
504
+ ],
505
+ "code_example": {
506
+ "lang": "html",
507
+ "before": "<audio src=\"podcast.mp3\" controls></audio>",
508
+ "after": "<audio src=\"podcast.mp3\" controls></audio>\n<p><a href=\"podcast-transcript.html\">Read the transcript</a></p>"
509
+ },
510
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/audio-only-and-video-only-prerecorded.html"
511
+ },
512
+ {
513
+ "criterion": "1.2.2",
514
+ "title": "Captions (Prerecorded)",
515
+ "level": "A",
516
+ "description": "All prerecorded video content with audio must have synchronized captions. Note: axe-core only checks that a <track kind=\"captions\"> element exists — it cannot verify caption accuracy, completeness, or synchronization.",
517
+ "steps": [
518
+ "Find all <video> elements on the page.",
519
+ "Verify a <track kind=\"captions\"> element is present inside each <video>.",
520
+ "Play the video and confirm captions appear, are synchronized with speech, and include non-speech audio cues (e.g., [music], [applause])."
521
+ ],
522
+ "remediation": [
523
+ "Add <track kind=\"captions\" src=\"captions.vtt\" srclang=\"en\" label=\"English\" default> inside each <video> element.",
524
+ "Generate accurate captions — auto-generated captions alone do not satisfy this criterion."
525
+ ],
526
+ "code_example": {
527
+ "lang": "html",
528
+ "before": "<video src=\"talk.mp4\" controls></video>",
529
+ "after": "<video src=\"talk.mp4\" controls>\n <track kind=\"captions\" src=\"talk-captions.vtt\" srclang=\"en\" label=\"English\" default>\n</video>"
530
+ },
531
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/captions-prerecorded.html"
532
+ },
533
+ {
534
+ "criterion": "1.2.5",
535
+ "title": "Audio Description (Prerecorded)",
536
+ "level": "AA",
537
+ "description": "Prerecorded video content must have an audio description track so that blind users can access information conveyed visually (actions, scene changes, on-screen text).",
538
+ "steps": [
539
+ "Watch the video with eyes closed.",
540
+ "Determine whether all meaningful visual information (actions, scene changes, on-screen text) can be understood from the audio alone.",
541
+ "If not, an audio description track is required."
542
+ ],
543
+ "remediation": [
544
+ "Add <track kind=\"descriptions\" src=\"descriptions.vtt\" srclang=\"en\" label=\"Audio Description\"> inside the <video> element.",
545
+ "Alternatively, provide a separate version of the video with audio descriptions mixed into the main audio track."
546
+ ],
547
+ "code_example": {
548
+ "lang": "html",
549
+ "before": "<video src=\"tutorial.mp4\" controls></video>",
550
+ "after": "<video src=\"tutorial.mp4\" controls>\n <track kind=\"descriptions\" src=\"tutorial-descriptions.vtt\" srclang=\"en\" label=\"Audio Description\">\n</video>"
551
+ },
552
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/audio-description-prerecorded.html"
553
+ },
554
+ {
555
+ "criterion": "1.3.3",
556
+ "title": "Sensory Characteristics",
557
+ "level": "A",
558
+ "description": "Instructions must not rely solely on sensory characteristics like shape, color, size, visual location, or sound. Users who are blind or color-blind cannot perceive these cues.",
559
+ "steps": [
560
+ "Find all instructional text: tooltips, help text, error messages, onboarding copy.",
561
+ "Check for phrases like \"click the round button\", \"see the red warning\", \"the menu on the left\", or \"press the large button\".",
562
+ "Verify each sensory reference has a non-sensory alternative such as a text label, role, or accessible name."
563
+ ],
564
+ "remediation": [
565
+ "Replace sensory-only references with text labels: use \"Click the Save button\" instead of \"Click the blue button on the right\".",
566
+ "Add aria-label or visible text to differentiate elements that look the same."
567
+ ],
568
+ "code_example": {
569
+ "lang": "html",
570
+ "before": "<p>Click the blue button to continue.</p>",
571
+ "after": "<p>Click the <strong>Continue</strong> button to proceed.</p>"
572
+ },
573
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/sensory-characteristics.html"
574
+ },
575
+ {
576
+ "criterion": "1.4.4",
577
+ "title": "Resize Text",
578
+ "level": "AA",
579
+ "description": "Text must be resizable up to 200% using browser zoom without loss of content or functionality. Fixed pixel font sizes and viewport-locked layouts break this.",
580
+ "steps": [
581
+ "Set browser zoom to 200% (Cmd+Plus on macOS / Ctrl+Plus on Windows).",
582
+ "Verify all text is readable and no content is clipped or hidden.",
583
+ "Confirm no horizontal scrolling is required (except for data tables or maps).",
584
+ "Verify all functionality remains operable at 200% zoom."
585
+ ],
586
+ "remediation": [
587
+ "Replace px font sizes with rem or em units.",
588
+ "Remove max-height constraints on elements that contain text.",
589
+ "Avoid overflow: hidden on text containers."
590
+ ],
591
+ "code_example": {
592
+ "lang": "css",
593
+ "before": "body { font-size: 14px; }\n.card { max-height: 100px; overflow: hidden; }",
594
+ "after": "body { font-size: 0.875rem; }\n.card { overflow: visible; }"
595
+ },
596
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/resize-text.html"
597
+ },
598
+ {
599
+ "criterion": "1.4.5",
600
+ "title": "Images of Text",
601
+ "level": "AA",
602
+ "description": "Text must not be rendered as images unless essential (e.g., logos, brand wordmarks). Images of text cannot be resized, recolored, or adapted by user stylesheets.",
603
+ "steps": [
604
+ "Inspect images on the page.",
605
+ "If an image contains readable text (not a logo), verify whether the same visual result can be achieved with real HTML text styled with CSS.",
606
+ "Right-click and Inspect — if it is an <img> or a background-image containing text, it may violate this criterion."
607
+ ],
608
+ "remediation": [
609
+ "Replace image-based text with real HTML text styled with CSS.",
610
+ "For decorative text effects, use CSS text-shadow, background-clip: text, or filter.",
611
+ "Logos and brand wordmarks are exempt from this criterion."
612
+ ],
613
+ "code_example": {
614
+ "lang": "html",
615
+ "before": "<img src=\"welcome-banner.png\" alt=\"Welcome to our site\" />",
616
+ "after": "<h1 class=\"banner-heading\">Welcome to our site</h1>"
617
+ },
618
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/images-of-text.html"
619
+ },
620
+ {
621
+ "criterion": "2.2.1",
622
+ "title": "Timing Adjustable",
623
+ "level": "A",
624
+ "description": "If a time limit is set for any content or interaction (session timeout, timed quiz, auto-logout), users must be able to turn it off, adjust it to at least 10 times the default, or extend it with at least 20 seconds warning before expiry.",
625
+ "steps": [
626
+ "Identify session timeouts, timed forms, auto-expiring modals, or sliding banners with time limits.",
627
+ "Trigger or simulate the timeout.",
628
+ "Check for a warning with enough time to extend the session.",
629
+ "Verify the extension mechanism is keyboard accessible."
630
+ ],
631
+ "remediation": [
632
+ "Add a warning dialog at least 20 seconds before session expiry with a keyboard-accessible \"Extend session\" button.",
633
+ "For non-essential time limits, provide a settings option to disable them entirely."
634
+ ],
635
+ "code_example": {
636
+ "lang": "js",
637
+ "before": "// No warning — user is logged out after 5 minutes with no notice\nsetTimeout(() => logout(), 5 * 60 * 1000);",
638
+ "after": "const WARNING_MS = 20 * 1000;\nconst TIMEOUT_MS = 5 * 60 * 1000;\n\nsetTimeout(() => showExtendSessionDialog(), TIMEOUT_MS - WARNING_MS);\n\nfunction showExtendSessionDialog() {\n // Render a keyboard-accessible dialog with \"Extend Session\" and \"Log out\" buttons\n}"
639
+ },
640
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html"
641
+ },
642
+ {
643
+ "criterion": "2.2.2",
644
+ "title": "Pause, Stop, Hide",
645
+ "level": "A",
646
+ "description": "Moving, blinking, scrolling, or auto-updating content that starts automatically and lasts more than 5 seconds must have a mechanism to pause, stop, or hide it.",
647
+ "steps": [
648
+ "Find carousels, hero sliders, scrolling tickers, auto-advancing slideshows, and auto-refreshing live data.",
649
+ "Verify a pause, stop, or hide button is visible and keyboard accessible for each."
650
+ ],
651
+ "remediation": [
652
+ "Add a visible \"Pause\" button to carousels and auto-advancing content.",
653
+ "For CSS animations running indefinitely, respect the prefers-reduced-motion media query.",
654
+ "For live data feeds, add a pause toggle so users can read content without it updating."
655
+ ],
656
+ "code_example": {
657
+ "lang": "html",
658
+ "before": "<div class=\"carousel\" data-autoplay=\"true\">\n <!-- slides -->\n</div>",
659
+ "after": "<div class=\"carousel\" data-autoplay=\"true\">\n <button type=\"button\" aria-label=\"Pause carousel\" class=\"carousel__pause\">&#9646;&#9646;</button>\n <!-- slides -->\n</div>"
660
+ },
661
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html"
662
+ },
663
+ {
664
+ "criterion": "2.3.1",
665
+ "title": "Three Flashes or Below Threshold",
666
+ "level": "A",
667
+ "description": "Content must not flash more than 3 times per second, as rapid flashing can trigger seizures in users with photosensitive epilepsy.",
668
+ "steps": [
669
+ "Review all animated content: GIFs, CSS animations, videos, and canvas animations.",
670
+ "Look for rapid alternating high-contrast flashing.",
671
+ "Use the PEAT tool (Photosensitivity Epilepsy Analysis Tool) for thorough analysis if in doubt."
672
+ ],
673
+ "remediation": [
674
+ "Limit animation flash rate to below 3 Hz.",
675
+ "Replace rapid alternating animations with smooth transitions.",
676
+ "Add prefers-reduced-motion guards for all animations."
677
+ ],
678
+ "code_example": {
679
+ "lang": "css",
680
+ "before": "@keyframes flash {\n 0%, 50% { background: white; }\n 51%, 100% { background: black; }\n}\n.alert { animation: flash 0.2s infinite; }",
681
+ "after": "@media (prefers-reduced-motion: no-preference) {\n @keyframes fade {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.5; }\n }\n .alert { animation: fade 0.8s ease-in-out infinite; }\n}"
682
+ },
683
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/three-flashes-or-below-threshold.html"
684
+ },
685
+ {
686
+ "criterion": "2.4.5",
687
+ "title": "Multiple Ways",
688
+ "level": "AA",
689
+ "description": "At least two ways must exist to locate a page within a set of pages (e.g., navigation + search, navigation + sitemap, navigation + related links).",
690
+ "steps": [
691
+ "Check if the site has at least two of: global navigation, search functionality, site map page, breadcrumb trail, or related pages links.",
692
+ "Verify the search field is keyboard accessible and has a visible label."
693
+ ],
694
+ "remediation": [
695
+ "Add a site-wide search field if one is not present.",
696
+ "Alternatively, add a sitemap page linked from the footer.",
697
+ "Breadcrumbs on deep pages also satisfy this criterion alongside main navigation."
698
+ ],
699
+ "code_example": {
700
+ "lang": "html",
701
+ "before": "<nav aria-label=\"Main\">\n <!-- nav links only -->\n</nav>",
702
+ "after": "<nav aria-label=\"Main\">\n <!-- nav links -->\n</nav>\n<form role=\"search\">\n <label for=\"site-search\">Search</label>\n <input id=\"site-search\" type=\"search\" />\n <button type=\"submit\">Search</button>\n</form>"
703
+ },
704
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/multiple-ways.html"
705
+ },
706
+ {
707
+ "criterion": "2.5.1",
708
+ "title": "Pointer Gestures",
709
+ "level": "A",
710
+ "description": "All functionality using multi-point gestures (pinch-to-zoom, two-finger swipe) or path-based gestures (swipe left/right) must have a single-pointer alternative (a button or click).",
711
+ "steps": [
712
+ "Identify map controls, carousels, sliders, and galleries.",
713
+ "Test whether all actions (zoom in/out, next/previous, drag to reposition) can be completed with a single tap or click using visible buttons."
714
+ ],
715
+ "remediation": [
716
+ "Add +/- buttons alongside pinch-to-zoom maps.",
717
+ "Add Previous/Next buttons for swipe-navigated carousels.",
718
+ "Add drag-handle buttons or keyboard alternatives for path-based interactions."
719
+ ],
720
+ "code_example": {
721
+ "lang": "html",
722
+ "before": "<div class=\"carousel\">\n <!-- swipe gesture only, no buttons -->\n</div>",
723
+ "after": "<div class=\"carousel\">\n <button type=\"button\" aria-label=\"Previous slide\">&#8249;</button>\n <!-- slides -->\n <button type=\"button\" aria-label=\"Next slide\">&#8250;</button>\n</div>"
724
+ },
725
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/pointer-gestures.html"
726
+ },
727
+ {
728
+ "criterion": "2.5.2",
729
+ "title": "Pointer Cancellation",
730
+ "level": "A",
731
+ "description": "Single-pointer actions must not be triggered on the down-event unless essential. Users need to be able to cancel accidental activations by moving the pointer away or releasing on a different element.",
732
+ "steps": [
733
+ "Test interactive elements (buttons, links, custom controls) — click and hold, then drag the pointer off the element before releasing.",
734
+ "Verify the action does not fire when the pointer is released outside the element.",
735
+ "Confirm buttons activate on mouseup/touchend (release), not mousedown/touchstart (press)."
736
+ ],
737
+ "remediation": [
738
+ "Use click events (which fire on release) instead of mousedown or pointerdown for triggering actions.",
739
+ "If pointerdown is essential, provide an abort or undo mechanism."
740
+ ],
741
+ "code_example": {
742
+ "lang": "js",
743
+ "before": "button.addEventListener('mousedown', () => deleteItem());",
744
+ "after": "// 'click' fires on mouseup — allows the user to move away to cancel\nbutton.addEventListener('click', () => deleteItem());"
745
+ },
746
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/pointer-cancellation.html"
747
+ },
748
+ {
749
+ "criterion": "3.2.1",
750
+ "title": "On Focus (No Context Change)",
751
+ "level": "A",
752
+ "description": "Receiving keyboard focus must not automatically trigger a context change (e.g., submitting a form, opening a new window, navigating to another page).",
753
+ "steps": [
754
+ "Tab through all interactive elements on the page.",
755
+ "Verify that receiving focus alone does not trigger navigation, form submission, alerts, or page reloads.",
756
+ "Select menus and dropdowns may open on focus but must not navigate until a selection is confirmed with Enter or a click."
757
+ ],
758
+ "remediation": [
759
+ "Move context-changing logic from focus events to change or click events.",
760
+ "If a dropdown opens on focus, ensure navigation requires an explicit Enter keypress or pointer click."
761
+ ],
762
+ "code_example": {
763
+ "lang": "js",
764
+ "before": "select.addEventListener('focus', () => navigate(select.value));",
765
+ "after": "select.addEventListener('change', () => navigate(select.value));"
766
+ },
767
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/on-focus.html"
768
+ },
769
+ {
770
+ "criterion": "3.2.2",
771
+ "title": "On Input (No Context Change)",
772
+ "level": "A",
773
+ "description": "Changing the value of a UI component must not automatically cause a context change without prior notification. Auto-submitting forms or navigating on dropdown change violates this.",
774
+ "steps": [
775
+ "Change select dropdowns, radio buttons, and checkboxes.",
776
+ "Verify no automatic navigation or form submission occurs on input change.",
777
+ "If auto-navigation does occur, check that the user was warned in advance."
778
+ ],
779
+ "remediation": [
780
+ "Move form submission or navigation logic to an explicit submit button.",
781
+ "If auto-submit is essential (e.g., a language selector), add visible notice: \"Selecting a language will reload the page.\""
782
+ ],
783
+ "code_example": {
784
+ "lang": "html",
785
+ "before": "<select onchange=\"location.href=this.value\">\n <option value=\"/en\">English</option>\n <option value=\"/es\">Español</option>\n</select>",
786
+ "after": "<select id=\"nav-select\">\n <option value=\"/en\">English</option>\n <option value=\"/es\">Español</option>\n</select>\n<button type=\"button\" onclick=\"navigate()\">Go</button>"
787
+ },
788
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/on-input.html"
789
+ },
790
+ {
791
+ "criterion": "3.2.3",
792
+ "title": "Consistent Navigation",
793
+ "level": "AA",
794
+ "description": "Navigation that repeats across pages must appear in the same relative order. Users with cognitive disabilities rely on consistent placement to predict where controls are.",
795
+ "steps": [
796
+ "Visit at least 3 different pages on the site.",
797
+ "Verify the main navigation, search bar, and any site-wide controls appear in the same relative order on each page.",
798
+ "Minor differences in active states are acceptable; reordering of items is not."
799
+ ],
800
+ "remediation": [
801
+ "Extract repeated navigation into a shared layout component.",
802
+ "Avoid reordering nav items based on page context or user actions.",
803
+ "If an item must be added for a specific page, append it at the end — do not rearrange existing items."
804
+ ],
805
+ "code_example": {
806
+ "lang": "html",
807
+ "before": "<!-- Page A -->\n<nav><a href=\"/\">Home</a><a href=\"/about\">About</a><a href=\"/contact\">Contact</a></nav>\n<!-- Page B (items reordered) -->\n<nav><a href=\"/contact\">Contact</a><a href=\"/\">Home</a><a href=\"/about\">About</a></nav>",
808
+ "after": "<!-- Both pages use the same nav component with consistent order -->\n<nav><a href=\"/\">Home</a><a href=\"/about\">About</a><a href=\"/contact\">Contact</a></nav>"
809
+ },
810
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/consistent-navigation.html"
811
+ },
812
+ {
813
+ "criterion": "3.2.4",
814
+ "title": "Consistent Identification",
815
+ "level": "AA",
816
+ "description": "Components with the same function must be identified consistently across pages (same label, same icon, same accessible name).",
817
+ "steps": [
818
+ "Find repeated interactive elements: search buttons, cart icons, login links, close buttons.",
819
+ "Verify their visible label and aria-label are identical across all pages.",
820
+ "A search icon labeled \"Search\" on one page and \"Find\" on another violates this criterion."
821
+ ],
822
+ "remediation": [
823
+ "Centralize labels for repeated components using constants or i18n keys.",
824
+ "Audit all aria-label values on shared components to ensure consistency."
825
+ ],
826
+ "code_example": {
827
+ "lang": "html",
828
+ "before": "<!-- Page A -->\n<button aria-label=\"Search\"><svg>...</svg></button>\n<!-- Page B -->\n<button aria-label=\"Find content\"><svg>...</svg></button>",
829
+ "after": "<!-- Both pages -->\n<button aria-label=\"Search\"><svg>...</svg></button>"
830
+ },
831
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/consistent-identification.html"
832
+ },
833
+ {
834
+ "criterion": "3.3.3",
835
+ "title": "Error Suggestion",
836
+ "level": "AA",
837
+ "description": "When an input error is identified, a suggestion for correction must be provided if possible (unless it would compromise security). Vague errors like \"Invalid input\" do not satisfy this.",
838
+ "steps": [
839
+ "Submit forms with intentional errors: wrong email format, empty required fields, invalid date.",
840
+ "Verify error messages explain what went wrong.",
841
+ "Confirm messages provide specific corrective guidance (e.g., \"Enter a valid email address, like name@example.com\")."
842
+ ],
843
+ "remediation": [
844
+ "Replace generic error messages with specific, actionable suggestions.",
845
+ "Include format hints in the error text.",
846
+ "Associate error messages with the relevant field using aria-describedby."
847
+ ],
848
+ "code_example": {
849
+ "lang": "html",
850
+ "before": "<input type=\"email\" id=\"email\" />\n<span class=\"error\">Invalid input</span>",
851
+ "after": "<input type=\"email\" id=\"email\" aria-describedby=\"email-error\" />\n<span id=\"email-error\" class=\"error\">Enter a valid email address — for example, name@example.com</span>"
852
+ },
853
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/error-suggestion.html"
854
+ },
855
+ {
856
+ "criterion": "3.3.4",
857
+ "title": "Error Prevention (Legal, Financial, Data)",
858
+ "level": "AA",
859
+ "description": "Pages that result in legal commitments, financial transactions, account deletion, or data submission must be reversible, verifiable, or confirmed before submission.",
860
+ "steps": [
861
+ "Identify checkout flows, form submissions with permanent effects, and account deletion flows.",
862
+ "Verify at least one of: a review/confirmation step before final submit, a way to correct data before finalizing, or a reversal mechanism (cancel order, undo delete)."
863
+ ],
864
+ "remediation": [
865
+ "Add a review step before checkout completion.",
866
+ "Add a confirmation dialog before irreversible actions such as account deletion or data purge.",
867
+ "Provide an order cancellation window (even 5 minutes) for financial transactions."
868
+ ],
869
+ "code_example": {
870
+ "lang": "html",
871
+ "before": "<button type=\"button\" onclick=\"deleteAccount()\">Delete Account</button>",
872
+ "after": "<!-- Step 1: trigger -->\n<button type=\"button\" onclick=\"showConfirmDialog()\">Delete Account</button>\n\n<!-- Step 2: confirmation dialog -->\n<dialog id=\"confirm-delete\">\n <p>Are you sure? This action cannot be undone.</p>\n <button type=\"button\" onclick=\"closeDialog()\">Cancel</button>\n <button type=\"button\" onclick=\"deleteAccount()\">Confirm Delete</button>\n</dialog>"
873
+ },
874
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/error-prevention-legal-financial-data.html"
875
+ },
876
+ {
877
+ "criterion": "1.2.4",
878
+ "title": "Captions (Live)",
879
+ "level": "AA",
880
+ "conditional": "Only applies if the site streams live audio or video content (e.g., webinars, live events, broadcasts). Mark as N/A if no live media is present.",
881
+ "description": "Live audio in synchronized media must have real-time captions that are accurate and synchronized with the audio.",
882
+ "steps": [
883
+ "Identify any live video or audio streams on the site.",
884
+ "Play the live stream and verify that real-time captions are visible or easily activatable.",
885
+ "Verify captions are reasonably accurate and stay synchronized with the speaker."
886
+ ],
887
+ "remediation": [
888
+ "Use a live captioning service (e.g., Google Live Captions, StreamText, or a professional CART provider) integrated with the streaming platform.",
889
+ "If using an embedded player (YouTube Live, Vimeo, etc.), enable the platform's built-in live caption feature."
890
+ ],
891
+ "code_example": {
892
+ "lang": "html",
893
+ "before": "<!-- BEFORE — live stream with no caption track -->\n<video autoplay controls src=\"https://stream.example.com/live.m3u8\"></video>",
894
+ "after": "<!-- AFTER — live stream with real-time caption track -->\n<video autoplay controls src=\"https://stream.example.com/live.m3u8\">\n <track kind=\"captions\" src=\"https://captions.example.com/live.vtt\" srclang=\"en\" label=\"English\" default>\n</video>"
895
+ },
896
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/captions-live.html"
897
+ },
898
+ {
899
+ "criterion": "1.4.2",
900
+ "title": "Audio Control",
901
+ "level": "A",
902
+ "conditional": "Only applies if the site plays audio automatically for more than 3 seconds. Mark as N/A if no auto-playing audio is present.",
903
+ "description": "If audio plays automatically for more than 3 seconds, the user must be able to pause or stop it, or control its volume independently from the system volume.",
904
+ "steps": [
905
+ "Load each page and note any audio that plays automatically.",
906
+ "If audio auto-plays for more than 3 seconds, verify a pause/stop control is visible at the top of the page or immediately reachable via keyboard.",
907
+ "Verify a volume control (if present) operates independently from the system volume."
908
+ ],
909
+ "remediation": [
910
+ "Set `autoplay` to false by default on all `<audio>` and `<video>` elements.",
911
+ "If auto-play is required, add `muted` and provide a visible unmute control.",
912
+ "Add a prominent pause/stop button visible in the page header or near the audio source."
913
+ ],
914
+ "code_example": {
915
+ "lang": "html",
916
+ "before": "<!-- BEFORE — audio auto-plays with no user control -->\n<audio autoplay src=\"background.mp3\"></audio>",
917
+ "after": "<!-- AFTER — muted by default with native controls -->\n<audio autoplay muted controls src=\"background.mp3\">\n Your browser does not support the audio element.\n</audio>"
918
+ },
919
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/audio-control.html"
920
+ },
921
+ {
922
+ "criterion": "2.5.4",
923
+ "title": "Motion Actuation",
924
+ "level": "A",
925
+ "conditional": "Only applies if the site uses device motion (shake, tilt, gyroscope) to trigger actions. Mark as N/A if no motion-based interactions are present.",
926
+ "description": "Functionality triggered by device motion must have a UI control alternative, and motion actuation must be disableable to prevent accidental triggering.",
927
+ "steps": [
928
+ "Identify any features that respond to device motion (shake, tilt, gyroscope gestures).",
929
+ "Test on a physical device: verify the same action can be performed via a visible UI control.",
930
+ "Verify there is a setting or toggle to disable motion-activated functions."
931
+ ],
932
+ "remediation": [
933
+ "Add a UI button or toggle that replicates any motion-triggered action.",
934
+ "Provide a setting to disable motion actuation entirely.",
935
+ "Check the `prefers-reduced-motion` media query and disable motion features when it is set."
936
+ ],
937
+ "code_example": {
938
+ "lang": "js",
939
+ "before": "// BEFORE — action only triggered by device motion\nwindow.addEventListener('devicemotion', (e) => {\n if (e.acceleration.x > 15) undoLastAction()\n})",
940
+ "after": "// AFTER — button alternative + motion can be disabled\nlet motionEnabled = true\ndocument.getElementById('undo-btn').addEventListener('click', undoLastAction)\nwindow.addEventListener('devicemotion', (e) => {\n if (motionEnabled && e.acceleration.x > 15) undoLastAction()\n})"
941
+ },
942
+ "ref": "https://www.w3.org/WAI/WCAG22/Understanding/motion-actuation.html"
943
+ }
944
+ ]