@surea11y/core 1.7.0 → 1.8.1

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.
Files changed (103) hide show
  1. package/CHANGELOG.md +103 -1
  2. package/README.md +157 -54
  3. package/docs/ACT_RULE_MAPPING.md +8 -7
  4. package/docs/API_STABILITY.md +18 -5
  5. package/docs/BINDING_AUTHORS_GUIDE.md +2 -2
  6. package/docs/CI_INTEGRATIONS.md +43 -0
  7. package/docs/DESIGN_CHALLENGES.md +97 -3
  8. package/docs/EARL.md +2 -2
  9. package/docs/ENGINE_OPTIONS.md +81 -3
  10. package/docs/I18N.md +62 -20
  11. package/docs/JUNIT.md +73 -0
  12. package/docs/LIMITATIONS.md +1 -0
  13. package/docs/OUTPUT_SCHEMA.md +19 -6
  14. package/docs/REPORT.md +7 -2
  15. package/docs/RULE_AUTHORING.md +73 -6
  16. package/docs/RULE_CATALOG.md +139 -116
  17. package/docs/RULE_EXAMPLES.md +2189 -0
  18. package/docs/RULE_HELPERS.md +62 -5
  19. package/docs/RULE_TAXONOMY.md +2 -2
  20. package/docs/SARIF.md +2 -1
  21. package/docs/WCAG_CONFORMANCE.md +56 -3
  22. package/package.json +34 -11
  23. package/profiles/index.js +14 -0
  24. package/src/checks/automatic/area-alt-present.js +87 -31
  25. package/src/checks/automatic/aria-braille-equivalent.js +25 -7
  26. package/src/checks/automatic/aria-hidden-focus.js +74 -18
  27. package/src/checks/automatic/aria-prohibited-attr.js +17 -4
  28. package/src/checks/automatic/aria-required-attr.js +29 -0
  29. package/src/checks/automatic/aria-role-name-present.js +19 -2
  30. package/src/checks/automatic/aria-valid-attr-value.js +28 -16
  31. package/src/checks/automatic/autocomplete-valid.js +26 -11
  32. package/src/checks/automatic/avoid-inline-spacing.js +105 -40
  33. package/src/checks/automatic/button-name-present.js +2 -1
  34. package/src/checks/automatic/canvas-text-alternative-present.js +105 -14
  35. package/src/checks/automatic/combobox-name-present.js +34 -51
  36. package/src/checks/automatic/contrast-computable.js +35 -4
  37. package/src/checks/automatic/contrast-enhanced.js +4 -4
  38. package/src/checks/automatic/contrast-minimum.js +45 -11
  39. package/src/checks/automatic/css-orientation-lock.js +152 -30
  40. package/src/checks/automatic/definition-list-children-valid.js +67 -23
  41. package/src/checks/automatic/deprecated-elements-not-used.js +43 -38
  42. package/src/checks/automatic/dialog-name-present.js +28 -9
  43. package/src/checks/automatic/duplicate-id.js +6 -2
  44. package/src/checks/automatic/identical-iframes-same-purpose.js +4 -4
  45. package/src/checks/automatic/iframe-focusable-content.js +7 -4
  46. package/src/checks/automatic/iframe-title-unique.js +36 -81
  47. package/src/checks/automatic/input-image-alt-present.js +32 -20
  48. package/src/checks/automatic/label-in-name.js +40 -13
  49. package/src/checks/automatic/language-page-present.js +12 -6
  50. package/src/checks/automatic/link-in-text-block.js +272 -60
  51. package/src/checks/automatic/link-name-present.js +13 -5
  52. package/src/checks/automatic/list-children-valid.js +18 -1
  53. package/src/checks/automatic/listbox-name-present.js +19 -49
  54. package/src/checks/automatic/listitem-parent-valid.js +4 -3
  55. package/src/checks/automatic/meta-refresh-no-exceptions.js +3 -3
  56. package/src/checks/automatic/page-title-present.js +16 -4
  57. package/src/checks/automatic/progressbar-name-present.js +11 -1
  58. package/src/checks/automatic/role-img-text-alternative-present.js +9 -5
  59. package/src/checks/automatic/searchbox-name-present.js +32 -49
  60. package/src/checks/automatic/server-side-image-map-absent.js +48 -28
  61. package/src/checks/automatic/slider-name-present.js +38 -52
  62. package/src/checks/automatic/spinbutton-name-present.js +32 -49
  63. package/src/checks/automatic/target-size-minimum.js +0 -11
  64. package/src/checks/automatic/td-has-header.js +41 -5
  65. package/src/checks/automatic/text-spacing-content-loss.js +548 -0
  66. package/src/checks/automatic/textbox-name-present.js +32 -49
  67. package/src/checks/automatic/valid-lang.js +15 -10
  68. package/src/checks/manual/area-alt-quality-manual.js +113 -31
  69. package/src/checks/manual/canvas-text-alternative-quality-manual.js +0 -2
  70. package/src/checks/manual/css-focus-indicator-suppressed-manual.js +23 -10
  71. package/src/checks/manual/css-hidden-focus.js +215 -7
  72. package/src/checks/manual/form-control-label-quality-manual.js +109 -5
  73. package/src/checks/manual/heading-order-manual.js +9 -1
  74. package/src/checks/manual/heading-quality-manual.js +143 -9
  75. package/src/checks/manual/img-alt-decorative-manual.js +6 -3
  76. package/src/checks/manual/input-image-alt-quality-manual.js +81 -13
  77. package/src/checks/manual/link-name-quality-manual.js +130 -4
  78. package/src/checks/manual/media-transcript-present-manual.js +65 -8
  79. package/src/checks/manual/mouse-only-event-handlers-manual.js +66 -5
  80. package/src/checks/manual/no-autoplay-audio-manual.js +93 -6
  81. package/src/checks/manual/p-as-heading-manual.js +89 -44
  82. package/src/checks/manual/page-title-patterns-manual.js +77 -8
  83. package/src/checks/manual/skip-link-manual.js +42 -14
  84. package/src/checks/manual/table-fake-caption-manual.js +32 -1
  85. package/src/checks/manual/video-caption-manual.js +47 -24
  86. package/src/checks/manual-review.js +0 -4
  87. package/src/core.js +14285 -2219
  88. package/src/coverage/en301549-map.js +187 -0
  89. package/src/coverage/standards.js +279 -0
  90. package/src/coverage/wcag-facets.js +1119 -0
  91. package/src/coverage/wcag-version-map.js +101 -0
  92. package/src/en301549.js +33 -0
  93. package/src/junit.js +321 -0
  94. package/src/profile-kit.js +163 -0
  95. package/src/report.js +343 -74
  96. package/src/sarif.js +34 -3
  97. package/src/wcag.js +105 -0
  98. package/surea11y.browser.js +5 -4
  99. package/surea11y.i18n.de.js +1 -1
  100. package/surea11y.i18n.es.js +1 -1
  101. package/surea11y.i18n.fr.js +1 -1
  102. package/surea11y.i18n.ja.js +3 -0
  103. package/src/checks/manual/area-alt-decorative-manual.js +0 -255
@@ -0,0 +1,2189 @@
1
+ # Rule examples
2
+
3
+ Hand-authored `Passed`/`Failed` (or, for manual rules, `Flagged`/`Not applicable`)
4
+ example pairs for every one of core's rules, meant to feed a future rule-page
5
+ docs site the way alfa.siteimprove.com/rules pages show worked examples
6
+ alongside a rule's description. Companion to [`RULE_CATALOG.md`](./RULE_CATALOG.md),
7
+ which carries each rule's title, WCAG mapping, applicability, and expectation —
8
+ this file adds one illustrative pair per rule, plus an extra `Flagged (cantTell)`
9
+ example where a rule's outcome is tiered. A profile's rules have theirs in the
10
+ profile.
11
+
12
+ Every example was verified against the built engine (`npm run build`, then
13
+ replayed through `tests/helpers/runa11yCoreOnHtml`), not just read off a
14
+ fixture's `.case-title` label or guessed from the rule's prose. Draft content:
15
+ hand-curated, not generated, and not yet checked by CI the way
16
+ `RULE_CATALOG.md` is. Once this feeds a real generator, it should get the
17
+ same treatment as the fixture-marker check (`scripts/generate-fixture-markers.js`):
18
+ a `--check` script wired into CI so a new or changed rule can't silently ship
19
+ without a matching example.
20
+
21
+ Manual rules (`type: 'manual'`) are capped at `cantTell`/`notApplicable` and
22
+ never return `pass`/`fail`; their pair below is `Flagged (cantTell)` /
23
+ `Not applicable` instead of `Passed`/`Failed`. A few automatic rules also
24
+ never reach `fail` in practice — each says so where it applies.
25
+
26
+ ## accesskeys
27
+
28
+ **Flagged (cantTell)**
29
+ ```html
30
+ <a href="/s1" accesskey="s">S1</a>
31
+ <a href="/s2" accesskey="s">S2</a>
32
+ ```
33
+ Two elements share the same `accesskey` value ("s"), which most browsers resolve unpredictably.
34
+
35
+ **Not applicable**
36
+ ```html
37
+ <a href="/a" accesskey="a">A</a>
38
+ <a href="/b" accesskey="b">B</a>
39
+ ```
40
+ Every `accesskey` value on the page is unique.
41
+
42
+ ## area-alt-present
43
+
44
+ **Passed**
45
+ ```html
46
+ <img src="image.png" alt="" usemap="#nav">
47
+ <map name="nav">
48
+ <area href="/x" shape="rect" coords="0,0,40,24" alt="Go to green">
49
+ </map>
50
+ ```
51
+ The `<area>` is a link inside a used map and has a non-empty `alt`.
52
+
53
+ **Failed**
54
+ ```html
55
+ <img src="image.png" alt="" usemap="#nav">
56
+ <map name="nav">
57
+ <area href="/x" shape="rect" coords="0,0,40,24">
58
+ </map>
59
+ ```
60
+ The `<area>` is a link (has `href`) in a used map but has no `alt`.
61
+
62
+ ## area-alt-quality
63
+
64
+ *Manual, and like its siblings below, flags every applicable element with a detected text alternative for review — it isn't selective about which ones look wrong. The flagged example shows a case where review genuinely matters, not a case the rule specifically detected as bad.*
65
+
66
+ **Flagged (cantTell)**
67
+ ```html
68
+ <img src="office-map.png" usemap="#floorplan">
69
+ <map name="floorplan">
70
+ <area href="/rooms/12" shape="rect" coords="0,0,40,24" alt="Room 12">
71
+ </map>
72
+ ```
73
+ The area has a non-empty text alternative in a used image map, worth confirming "Room 12" is accurate for that region. An `aria-label`, `aria-labelledby` or `title` is asked about the same way, and each occurrence lists the sources it found.
74
+
75
+ **Not applicable**
76
+ ```html
77
+ <map name="unused">
78
+ <area href="/rooms/12" shape="rect" coords="0,0,40,24" alt="Room 12">
79
+ </map>
80
+ ```
81
+ No `<img usemap>` references this map, so the area is never hit-tested or rendered — out of scope.
82
+
83
+ ## aria-allowed-attr
84
+
85
+ **Passed**
86
+ ```html
87
+ <div role="checkbox" aria-checked="true"></div>
88
+ ```
89
+ `aria-checked` is one of the states role="checkbox" supports.
90
+
91
+ **Failed**
92
+ ```html
93
+ <div role="checkbox" aria-valuenow="1"></div>
94
+ ```
95
+ `aria-valuenow` belongs to range widgets, not role="checkbox".
96
+
97
+ ## aria-allowed-role
98
+
99
+ *Despite being `type: 'automatic'`, this rule never returns `fail` — no ACT rule and no WCAG mapping exist for it, so a disallowed role is raised for review rather than decided outright.*
100
+
101
+ **Passed**
102
+ ```html
103
+ <nav role="navigation">...</nav>
104
+ ```
105
+ `role="navigation"` is permitted on `<nav>` by the ARIA-in-HTML spec (it's also the element's implicit role).
106
+
107
+ **Flagged (cantTell)**
108
+ ```html
109
+ <nav role="tab">...</nav>
110
+ ```
111
+ `role="tab"` is not one of the roles ARIA-in-HTML permits on `<nav>`.
112
+
113
+ ## aria-braille-equivalent
114
+
115
+ **Passed**
116
+ ```html
117
+ <button aria-braillelabel="SAVE">Save changes</button>
118
+ ```
119
+ The braille label supplements a real accessible name ("Save changes"); non-braille assistive technology still gets a name.
120
+
121
+ **Flagged (cantTell)**
122
+ ```html
123
+ <button aria-braillelabel="SAVE"></button>
124
+ ```
125
+ `aria-braillelabel` is the only naming mechanism present; non-braille assistive technology gets no accessible name at all.
126
+
127
+ ## aria-checked-state-mismatch
128
+
129
+ **Flagged (cantTell)**
130
+ ```html
131
+ <input type="checkbox" checked aria-checked="false">
132
+ ```
133
+ The checkbox is actually checked, but `aria-checked="false"` tells assistive technology the opposite.
134
+
135
+ **Not applicable**
136
+ ```html
137
+ <input type="checkbox" checked aria-checked="true">
138
+ ```
139
+ `aria-checked` matches the control's real checked state.
140
+
141
+ ## aria-conditional-attr
142
+
143
+ **Passed**
144
+ ```html
145
+ <input type="text" aria-invalid="true" aria-errormessage="err1">
146
+ <span id="err1">Enter a valid email address.</span>
147
+ ```
148
+ `aria-invalid="true"` means `aria-errormessage` is actually exposed to assistive technology.
149
+
150
+ **Flagged (cantTell)**
151
+ ```html
152
+ <input type="text" aria-errormessage="err1">
153
+ <span id="err1">Enter a valid email address.</span>
154
+ ```
155
+ Without `aria-invalid` set to a non-`"false"` value, `aria-errormessage` is never exposed — the reference is currently inert.
156
+
157
+ ## aria-deprecated-role
158
+
159
+ **Passed**
160
+ ```html
161
+ <ul role="list"><li>Item</li></ul>
162
+ ```
163
+ `role="list"` is current, non-deprecated ARIA.
164
+
165
+ **Flagged (cantTell)**
166
+ ```html
167
+ <div role="directory"><a href="/a">Item</a></div>
168
+ ```
169
+ `role="directory"` is deprecated in WAI-ARIA in favor of `role="list"`; still valid markup, but discouraged.
170
+
171
+ ## aria-hidden-body
172
+
173
+ **Passed**
174
+ ```html
175
+ <body>Hi</body>
176
+ ```
177
+ The document `<body>` is not `aria-hidden`.
178
+
179
+ **Failed**
180
+ ```html
181
+ <body aria-hidden="true">Hi</body>
182
+ ```
183
+ Hiding `<body>` removes the entire page from the accessibility tree at once.
184
+
185
+ ## aria-hidden-focus
186
+
187
+ **Passed**
188
+ ```html
189
+ <div aria-hidden="true">
190
+ <p>Decorative text</p>
191
+ </div>
192
+ ```
193
+ Nothing inside the hidden subtree is focusable, so hiding it from assistive tech leaves no dead focus stop.
194
+
195
+ **Failed**
196
+ ```html
197
+ <div aria-hidden="true">
198
+ <a href="/x">Link</a>
199
+ </div>
200
+ ```
201
+ The link stays in the tab order even though `aria-hidden` removes it from the accessibility tree.
202
+
203
+ ## aria-prohibited-attr
204
+
205
+ **Passed**
206
+ ```html
207
+ <div role="generic"></div>
208
+ ```
209
+ role="generic" cannot be named, and none of the naming attributes are present.
210
+
211
+ **Failed**
212
+ ```html
213
+ <div role="generic" aria-label="Something"></div>
214
+ ```
215
+ `aria-label` is set on a role that WAI-ARIA prohibits from being named.
216
+
217
+ ## aria-prohibited-children
218
+
219
+ **Passed**
220
+ ```html
221
+ <ul role="menubar">
222
+ <li role="menuitem">File</li>
223
+ <li role="menuitem">Edit</li>
224
+ </ul>
225
+ ```
226
+ Both owned children are roles menubar's spec allows.
227
+
228
+ **Failed**
229
+ ```html
230
+ <div role="table">
231
+ <div role="row"><div role="cell">x</div></div>
232
+ <div role="button">Not a valid table child</div>
233
+ </div>
234
+ ```
235
+ role="table" only allows row (or rowgroup) children; the button is a disallowed owned child.
236
+
237
+ ## aria-required-attr
238
+
239
+ **Passed**
240
+ ```html
241
+ <div role="checkbox" aria-checked="true"></div>
242
+ ```
243
+ role="checkbox" requires `aria-checked`, and it's present.
244
+
245
+ **Failed**
246
+ ```html
247
+ <div role="checkbox"></div>
248
+ ```
249
+ role="checkbox" requires `aria-checked`; it's missing entirely.
250
+
251
+ ## aria-required-children
252
+
253
+ **Passed**
254
+ ```html
255
+ <ul role="list"><li>Item</li></ul>
256
+ ```
257
+ The list role owns at least one `listitem`-role child.
258
+
259
+ **Flagged (cantTell)**
260
+ ```html
261
+ <div role="list"></div>
262
+ ```
263
+ An empty `role="list"` owns no required child role — announced as a list with no items, which is what it is, but worth a look.
264
+
265
+ ## aria-required-parent
266
+
267
+ **Passed**
268
+ ```html
269
+ <div role="tablist">
270
+ <div role="tab">Overview</div>
271
+ </div>
272
+ ```
273
+ role="tab" requires an ancestor (or `aria-owns` owner) with role="tablist", and one is present.
274
+
275
+ **Failed**
276
+ ```html
277
+ <nav role="tab"></nav>
278
+ ```
279
+ role="tab" has no tablist ancestor or owner anywhere on the page.
280
+
281
+ ## aria-role-name-present
282
+
283
+ **Passed**
284
+ ```html
285
+ <div role="grid" aria-label="Orders"></div>
286
+ ```
287
+ role="grid" requires an accessible name, and `aria-label` provides one.
288
+
289
+ **Failed**
290
+ ```html
291
+ <div role="grid"></div>
292
+ ```
293
+ role="grid" requires an accessible name; none is present.
294
+
295
+ ## aria-roles-valid
296
+
297
+ **Passed**
298
+ ```html
299
+ <div role="button">Click</div>
300
+ ```
301
+ "button" is a real, concrete ARIA role.
302
+
303
+ **Failed**
304
+ ```html
305
+ <div role="buton"></div>
306
+ ```
307
+ "buton" isn't a defined ARIA role token (typo).
308
+
309
+ ## aria-text
310
+
311
+ **Flagged (cantTell)**
312
+ ```html
313
+ <span role="text">Some text with <a href="#">a link</a> inside it.</span>
314
+ ```
315
+ `role="text"` tells assistive technology to treat this as a single flat string, but the nested link is still focusable and interactive underneath it.
316
+
317
+ **Not applicable**
318
+ ```html
319
+ <span role="text">Plain text with no interactive content.</span>
320
+ ```
321
+ No focusable descendant exists inside the `role="text"` element.
322
+
323
+ ## aria-valid-attr
324
+
325
+ *Despite being `type: 'automatic'`, this rule never returns `fail` — an aria-* attribute the spec doesn't define is inert, so nothing about the element's exposed name, role, or value actually changes.*
326
+
327
+ **Passed**
328
+ ```html
329
+ <button aria-label="Save">Save</button>
330
+ ```
331
+ `aria-label` is a real, defined ARIA attribute.
332
+
333
+ **Flagged (cantTell)**
334
+ ```html
335
+ <button aria-labeledby="x">Save</button>
336
+ ```
337
+ `aria-labeledby` is a typo (the real attribute is `aria-labelledby`) and is silently ignored by assistive technology.
338
+
339
+ ## aria-valid-attr-value
340
+
341
+ **Passed**
342
+ ```html
343
+ <div aria-hidden="true"></div>
344
+ ```
345
+ "true" is a valid value for the boolean-typed `aria-hidden`.
346
+
347
+ **Failed**
348
+ ```html
349
+ <div aria-hidden="yes"></div>
350
+ ```
351
+ `aria-hidden` is boolean-typed; "yes" isn't a valid boolean token.
352
+
353
+ ## autocomplete-valid
354
+
355
+ **Passed**
356
+ ```html
357
+ <input autocomplete="email">
358
+ ```
359
+ "email" is a recognized autofill field-name token.
360
+
361
+ **Failed**
362
+ ```html
363
+ <input autocomplete="emial">
364
+ ```
365
+ "emial" is a typo, not a recognized autofill token.
366
+
367
+ ## avoid-inline-spacing
368
+
369
+ **Passed**
370
+ ```html
371
+ <p style="letter-spacing:2px !important">Spaced text</p>
372
+ ```
373
+ The forced letter-spacing is above the WCAG 1.4.12 minimum metric.
374
+
375
+ **Failed**
376
+ ```html
377
+ <p style="line-height:1.2 !important">The toy brought back fond memories of being lost in the rain forest.</p>
378
+ ```
379
+ `!important` forces line-height below the 1.5 minimum, and the page's own styles can't override it.
380
+
381
+ **Flagged (cantTell)**
382
+ ```html
383
+ <p style="line-height:1 !important">Opening hours</p>
384
+ ```
385
+ The text is short enough to fit on one line, so it may never wrap, and the criterion applies only to text that wraps.
386
+
387
+ ## binary-control-name-present
388
+
389
+ **Passed**
390
+ ```html
391
+ <div role="switch" tabindex="0" aria-checked="false" aria-label="Bluetooth"></div>
392
+ ```
393
+ `aria-label` gives the switch an accessible name.
394
+
395
+ **Failed**
396
+ ```html
397
+ <div role="switch" tabindex="0" aria-checked="false"></div>
398
+ ```
399
+ The switch has no name from any supported mechanism.
400
+
401
+ ## button-name-present
402
+
403
+ **Passed**
404
+ ```html
405
+ <button>Save</button>
406
+ ```
407
+ The button's text content is its accessible name.
408
+
409
+ **Failed**
410
+ ```html
411
+ <button></button>
412
+ ```
413
+ The button has no text content, `aria-label`, or `aria-labelledby`.
414
+
415
+ ## bypass-blocks-present
416
+
417
+ **Not applicable**
418
+ ```html
419
+ <body>
420
+ <nav>Site nav</nav>
421
+ <main>Primary content</main>
422
+ </body>
423
+ ```
424
+ A main landmark exists — a recognized bypass mechanism, so there's nothing to flag.
425
+
426
+ **Flagged (cantTell)**
427
+ ```html
428
+ <body>
429
+ <nav>Site nav</nav>
430
+ <div>Primary content, no landmark, no heading, no skip link.</div>
431
+ </body>
432
+ ```
433
+ No main landmark, working same-page anchor link, or visible heading was detected — a review prompt, not a fail, since the engine can't confirm from one snapshot whether the page truly lacks a bypass mechanism.
434
+
435
+ ## canvas-text-alternative-present
436
+
437
+ **Passed**
438
+ ```html
439
+ <canvas>Chart summary</canvas>
440
+ ```
441
+ Fallback content between the tags provides a text alternative.
442
+
443
+ **Failed**
444
+ ```html
445
+ <canvas></canvas>
446
+ ```
447
+ No fallback content and no accessible name.
448
+
449
+ ## canvas-text-alternative-quality
450
+
451
+ *Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.*
452
+
453
+ **Flagged (cantTell)**
454
+ ```html
455
+ <canvas width="400" height="200">Bar chart</canvas>
456
+ ```
457
+ The fallback text "Bar chart" is present but generic — worth confirming it conveys the actual data, not just the chart type.
458
+
459
+ **Not applicable**
460
+ ```html
461
+ <canvas width="400" height="200"></canvas>
462
+ ```
463
+ No fallback content, ARIA name, or title exists to review; that's `canvas-text-alternative-present`'s failure instead.
464
+
465
+ ## combobox-name-present
466
+
467
+ **Passed**
468
+ ```html
469
+ <div role="combobox" tabindex="0" aria-label="Country"></div>
470
+ ```
471
+ `aria-label` names the combobox.
472
+
473
+ **Failed**
474
+ ```html
475
+ <div role="combobox" tabindex="0"></div>
476
+ ```
477
+ No accessible name from any supported mechanism.
478
+
479
+ ## contrast-computable
480
+
481
+ *This rule is a computability gate: it never returns `fail`, only `pass` (a ratio was computed), `cantTell` (no ratio could be computed at all), or `notApplicable`.*
482
+
483
+ **Passed**
484
+ ```html
485
+ <p style="background:#ffffff; color:#999999;">Body text</p>
486
+ ```
487
+ Foreground and background are both flat, opaque colors, so a ratio can be computed (whether or not it later passes AA/AAA).
488
+
489
+ **Flagged (cantTell)**
490
+ ```html
491
+ <p style="background-image:url(texture.png); color:#000000;">Body text</p>
492
+ ```
493
+ A `background-image` behind the text is not a flat color, so no contrast ratio can be computed at all.
494
+
495
+ ## contrast-enhanced
496
+
497
+ **Passed**
498
+ ```html
499
+ <p style="background:#ffffff; color:#000000;">Body text</p>
500
+ ```
501
+ Black on white is roughly 21:1, well above the AAA 7:1 threshold.
502
+
503
+ **Failed**
504
+ ```html
505
+ <p style="background:#ffffff; color:#767676;">Body text</p>
506
+ ```
507
+ Gray on white is about 4.6:1 — passes the AA minimum but fails the stricter 7:1 AAA threshold.
508
+
509
+ ## contrast-minimum
510
+
511
+ **Passed**
512
+ ```html
513
+ <p style="background:#ffffff; color:#767676;">Body text</p>
514
+ ```
515
+ About 4.6:1, above the AA 4.5:1 minimum for normal text.
516
+
517
+ **Failed**
518
+ ```html
519
+ <div style="opacity:0.5; background:#ffffff;">
520
+ <p style="color:#000000;">Body text</p>
521
+ </div>
522
+ ```
523
+ 50% ancestor (group) opacity over white composites to a flat gray around 4:1, below the AA minimum.
524
+
525
+ ## css-focus-indicator-suppressed
526
+
527
+ **Flagged (cantTell)**
528
+ ```html
529
+ <style>.reset-only:focus { outline: none; }</style>
530
+ <a class="reset-only" href="/pricing">Pricing</a>
531
+ ```
532
+ `:focus { outline: none; }` removes the focus indicator, and no other rule matching this link draws a replacement.
533
+
534
+ **Flagged (cantTell)**
535
+ ```html
536
+ <style>a { outline: none; }</style>
537
+ <a href="/news">News</a>
538
+ ```
539
+ A rule with no `:focus` still applies while the link has focus, and it outranks the browser's own focus outline (F78). No focus rule draws a replacement.
540
+
541
+ **Not applicable**
542
+ ```html
543
+ <style>.reset-shadow:focus { outline: none; box-shadow: 0 0 0 3px navy; }</style>
544
+ <button class="reset-shadow" type="button">Send</button>
545
+ ```
546
+ The same rule that removes the outline draws a `box-shadow` in its place.
547
+
548
+ ## css-hidden-focus
549
+
550
+ **Flagged (cantTell)**
551
+ ```html
552
+ <a href="/x" style="opacity:0;">Hidden but tabbable link</a>
553
+ ```
554
+ The link stays in the tab order despite `opacity:0` making it invisible to sighted keyboard users.
555
+
556
+ **Not applicable**
557
+ ```html
558
+ <a href="/x">Visible link</a>
559
+ ```
560
+ The link is both focusable and visible — nothing to flag.
561
+
562
+ **Not applicable**
563
+ ```html
564
+ <style>.skip { position: absolute; left: -9999px; } .skip:focus { left: 0; }</style>
565
+ <a class="skip" href="#main">Skip to content</a>
566
+ ```
567
+ The link is off-screen at rest, but its `:focus` rule brings it back into view when it takes focus.
568
+
569
+ ## css-orientation-lock
570
+
571
+ **Passed**
572
+ ```html
573
+ <html>
574
+ <head>
575
+ <title>Page title</title>
576
+ <style>body { background: #fff; }</style>
577
+ </head>
578
+ <body>Hi</body>
579
+ </html>
580
+ ```
581
+ No orientation media query rotates the page.
582
+
583
+ **Failed**
584
+ ```html
585
+ <html>
586
+ <head>
587
+ <title>Page title</title>
588
+ <style>
589
+ @media (orientation: landscape) {
590
+ body { transform: rotate(90deg); }
591
+ }
592
+ </style>
593
+ </head>
594
+ <body>Hi</body>
595
+ </html>
596
+ ```
597
+ A landscape media query forces a 90-degree rotation, locking the page to portrait regardless of device orientation.
598
+
599
+ ## definition-list-children-valid
600
+
601
+ **Passed**
602
+ ```html
603
+ <dl>
604
+ <dt>Term</dt>
605
+ <dd>Definition</dd>
606
+ </dl>
607
+ ```
608
+ `<dl>`'s only direct children are `dt`/`dd`.
609
+
610
+ **Failed**
611
+ ```html
612
+ <dl>
613
+ <dt>Term</dt>
614
+ <dd>Definition</dd>
615
+ <p>oops</p>
616
+ </dl>
617
+ ```
618
+ The `<p>` is a direct child of `<dl>` that isn't `dt`/`dd` (or an allowed wrapper).
619
+
620
+ ## deprecated-elements-not-used
621
+
622
+ **Passed**
623
+ ```html
624
+ <p>Normal content</p>
625
+ <blink>Sale</blink>
626
+ ```
627
+ No `<marquee>` anywhere, which satisfies the rule outright; there is no separate not-applicable case. `<blink>` is not reported, since no browser makes it blink.
628
+
629
+ **Flagged (cantTell)**
630
+ ```html
631
+ <marquee>Breaking news</marquee>
632
+ ```
633
+ `<marquee>` scrolls its content and gives the user no way to pause, stop, or hide it; whether the page adds its own control is for a person to check. This rule never fails.
634
+
635
+ ## dialog-name-present
636
+
637
+ **Passed**
638
+ ```html
639
+ <div role="dialog" aria-label="Confirm deletion">
640
+ <p>Are you sure?</p>
641
+ </div>
642
+ ```
643
+ `aria-label` names the dialog.
644
+
645
+ **Failed**
646
+ ```html
647
+ <div role="dialog">
648
+ <p>Body text only.</p>
649
+ </div>
650
+ ```
651
+ No accessible name; visible body text doesn't count toward a dialog's name.
652
+
653
+ ## dlitem-parent-valid
654
+
655
+ **Passed**
656
+ ```html
657
+ <dl>
658
+ <dt>Term</dt>
659
+ <dd>Definition</dd>
660
+ </dl>
661
+ ```
662
+ Both `dt` and `dd` are inside a `<dl>` ancestor.
663
+
664
+ **Failed**
665
+ ```html
666
+ <div>
667
+ <dt>orphan</dt>
668
+ </div>
669
+ ```
670
+ `<dt>` has no `<dl>` ancestor at all.
671
+
672
+ ## duplicate-id
673
+
674
+ *A duplicate id is a real defect under WCAG 2.0/2.1 (SC 4.1.1), but that criterion was removed in 2.2, so under this engine's default 2.2 target the same defect is coerced to `cantTell` (with a `wcagVersionScope` field) instead of `fail`. The failed example below targets `engineOptions.wcagVersion: '2.0'` to show the rule's underlying decision directly.*
675
+
676
+ **Passed**
677
+ ```html
678
+ <div id="section-1">First</div>
679
+ <div id="section-2">Second</div>
680
+ ```
681
+ Each element has a distinct id.
682
+
683
+ **Failed** (with `engineOptions.wcagVersion: '2.0'`)
684
+ ```html
685
+ <div id="section-1">First</div>
686
+ <div id="section-1">Second</div>
687
+ ```
688
+ The id `section-1` is used by two elements in the same tree, breaking `<label for>`, fragment links, and `getElementById`.
689
+
690
+ ## duplicate-id-aria
691
+
692
+ *Despite being `type: 'automatic'`, this rule never returns `fail` — which element an `aria-labelledby`/`aria-describedby` reference resolves to first is well-defined, but whether that's the intended target depends on author intent, which markup doesn't carry.*
693
+
694
+ **Passed**
695
+ ```html
696
+ <span id="lbl">Name</span>
697
+ <input type="text" aria-labelledby="lbl">
698
+ ```
699
+ The id `lbl` is unique, so the reference resolves unambiguously.
700
+
701
+ **Flagged (cantTell)**
702
+ ```html
703
+ <span id="lbl">Name</span>
704
+ <span id="lbl">Duplicate</span>
705
+ <input type="text" aria-labelledby="lbl">
706
+ ```
707
+ `aria-labelledby="lbl"` resolves to the first element with that id — whether that's the intended target is an authoring question the markup can't answer.
708
+
709
+ ## embed-text-alternative-present
710
+
711
+ **Passed**
712
+ ```html
713
+ <embed src="report.pdf" type="application/pdf" aria-label="Quarterly report PDF">
714
+ ```
715
+ `aria-label` gives the embed an accessible name.
716
+
717
+ **Failed**
718
+ ```html
719
+ <embed src="report.pdf" type="application/pdf">
720
+ ```
721
+ No accessible name and no fallback content.
722
+
723
+ ## embed-text-alternative-quality
724
+
725
+ *Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.*
726
+
727
+ **Flagged (cantTell)**
728
+ ```html
729
+ <embed src="report.pdf" type="application/pdf" title="Quarterly report PDF">
730
+ ```
731
+ A `title` is present — worth confirming it identifies the document well enough for someone who can't see the embedded viewer.
732
+
733
+ **Not applicable**
734
+ ```html
735
+ <embed src="report.pdf" type="application/pdf">
736
+ ```
737
+ No aria-label, resolved aria-labelledby, or title exists to review; that's `embed-text-alternative-present`'s failure instead.
738
+
739
+ ## empty-heading
740
+
741
+ **Flagged (cantTell)**
742
+ ```html
743
+ <h2></h2>
744
+ ```
745
+ The heading has no accessible name at all — nothing for a screen reader to announce.
746
+
747
+ **Not applicable**
748
+ ```html
749
+ <h2>Section title</h2>
750
+ ```
751
+ The heading has visible text content.
752
+
753
+ ## empty-table-header
754
+
755
+ **Flagged (cantTell)**
756
+ ```html
757
+ <table><tr><th></th></tr></table>
758
+ ```
759
+ The header cell has no text content and no accessible name.
760
+
761
+ **Not applicable**
762
+ ```html
763
+ <table><tr><th>Name</th></tr></table>
764
+ ```
765
+ The header cell has visible text content.
766
+
767
+ ## focus-order-semantics
768
+
769
+ **Flagged (cantTell)**
770
+ ```html
771
+ <div role="heading" tabindex="0">Fake focusable heading</div>
772
+ ```
773
+ A heading role has no interactive behavior, so making it tabbable is unusual and worth a look.
774
+
775
+ **Not applicable**
776
+ ```html
777
+ <div role="button" tabindex="0">Real custom button</div>
778
+ ```
779
+ `role="button"` is an interactive role, so being tabbable is expected.
780
+
781
+ ## form-control-label-quality
782
+
783
+ *This rule is scoped to fields that already have a visible programmatic label; a field with no visible label at all belongs to `form-control-programmatic-label-present`/`form-control-programmatic-label-quality` instead.*
784
+
785
+ **Flagged (cantTell)**
786
+ ```html
787
+ <label>Label <input type="text" name="f1"></label>
788
+ ```
789
+ The label text is the placeholder word "Label" itself, not a real description of the field.
790
+
791
+ **Not applicable**
792
+ ```html
793
+ <label for="e">Email address:</label>
794
+ <input id="e" type="text">
795
+ ```
796
+ The label text is a genuine, descriptive name for the field.
797
+
798
+ ## form-control-programmatic-label-present
799
+
800
+ **Passed**
801
+ ```html
802
+ <label for="email">Full name</label>
803
+ <input id="email" type="text">
804
+ ```
805
+ The `<label for>` association gives the input a programmatic label.
806
+
807
+ **Failed**
808
+ ```html
809
+ <input type="text">
810
+ ```
811
+ No `<label>`, `aria-label`, or `aria-labelledby` — nothing programmatic names the field.
812
+
813
+ ## form-control-programmatic-label-quality
814
+
815
+ **Flagged (cantTell)**
816
+ ```html
817
+ <input type="text" placeholder="Email address">
818
+ ```
819
+ The control's only name comes from `placeholder`, which disappears once the user starts typing.
820
+
821
+ **Not applicable**
822
+ ```html
823
+ <label for="email">Email address</label>
824
+ <input type="text" id="email" placeholder="name@example.com">
825
+ ```
826
+ A persistent `<label>` provides the name; `placeholder` is just a format hint here.
827
+
828
+ ## form-control-single-label
829
+
830
+ **Passed**
831
+ ```html
832
+ <label>Name <input type="text"></label>
833
+ ```
834
+ A single wrapping label; only one labelling mechanism applies.
835
+
836
+ **Failed**
837
+ ```html
838
+ <label for="address">Address</label>
839
+ <label for="address">Mailing address</label>
840
+ <input id="address" type="text">
841
+ ```
842
+ Two separate `label[for]` elements both target the same control.
843
+
844
+ ## heading-order
845
+
846
+ **Flagged (cantTell)**
847
+ ```html
848
+ <h1>Title</h1>
849
+ <h3>Subsection</h3>
850
+ ```
851
+ The heading level jumps from 1 to 3, skipping level 2.
852
+
853
+ **Not applicable**
854
+ ```html
855
+ <h1>Title</h1>
856
+ <h2>Section</h2>
857
+ <h3>Subsection</h3>
858
+ ```
859
+ Each heading level increases by at most one at a time.
860
+
861
+ ## heading-quality
862
+
863
+ *Headings with no accessible name at all belong to `empty-heading`, not this rule.*
864
+
865
+ **Flagged (cantTell)**
866
+ ```html
867
+ <h3>Heading</h3>
868
+ <p>We are open Monday through Friday from 10 to 16.</p>
869
+ ```
870
+ The heading text is the generic placeholder word "Heading" itself, not a description of the section.
871
+
872
+ **Not applicable**
873
+ ```html
874
+ <h3>Opening hours</h3>
875
+ <p>We are open Monday through Friday from 10 to 16.</p>
876
+ ```
877
+ The heading names its actual topic.
878
+
879
+ ## html-lang-attr-present
880
+
881
+ **Passed**
882
+ ```html
883
+ <html lang="en">
884
+ <head><title>Page title</title></head>
885
+ <body>Hi</body>
886
+ </html>
887
+ ```
888
+ A non-empty, syntactically valid language tag is declared.
889
+
890
+ **Failed**
891
+ ```html
892
+ <html>
893
+ <head><title>Page title</title></head>
894
+ <body>Hi</body>
895
+ </html>
896
+ ```
897
+ No `lang` attribute at all on `<html>`.
898
+
899
+ ## html-xml-lang-mismatch
900
+
901
+ **Passed**
902
+ ```html
903
+ <html lang="en-US" xml:lang="en-GB">
904
+ ```
905
+ Primary subtags agree (`en`); regional subtags are allowed to differ.
906
+
907
+ **Failed**
908
+ ```html
909
+ <html lang="en" xml:lang="fr">
910
+ ```
911
+ `lang` and `xml:lang` declare different primary languages.
912
+
913
+ ## identical-iframes-same-purpose
914
+
915
+ *Despite being `type: 'automatic'`, this rule never returns `fail` — frames sharing a name but resolving to different URLs can still be legitimately equivalent, and nothing in the markup settles that either way.*
916
+
917
+ **Passed**
918
+ ```html
919
+ <iframe title="Weather widget" src="https://example.test/weather"></iframe>
920
+ <iframe title="Weather widget" src="https://example.test/weather"></iframe>
921
+ ```
922
+ Both frames share the name "Weather widget" and embed the same resource.
923
+
924
+ **Flagged (cantTell)**
925
+ ```html
926
+ <iframe title="Weather widget" src="https://example.test/weather?city=nyc"></iframe>
927
+ <iframe title="Weather widget" src="https://example.test/weather?city=sf"></iframe>
928
+ ```
929
+ Both frames share the same name but resolve to different resources — possibly two legitimately equivalent widgets, possibly a mislabeled frame; the markup alone doesn't settle it.
930
+
931
+ ## identical-links-same-purpose
932
+
933
+ **Flagged (cantTell)**
934
+ ```html
935
+ <a href="/articles/1">Read more</a>
936
+ <a href="/articles/2">Read more</a>
937
+ ```
938
+ Both links share the accessible name "Read more" but resolve to different destinations.
939
+
940
+ **Not applicable**
941
+ ```html
942
+ <a href="/articles/1">Read more: Quarterly results</a>
943
+ <a href="/articles/2">Read more: Product launch</a>
944
+ ```
945
+ Each link's accessible name is distinct, so there's no same-name group to compare destinations within.
946
+
947
+ ## iframe-focusable-content
948
+
949
+ **Passed**
950
+ ```html
951
+ <iframe tabindex="-1" width="300" height="150" srcdoc="<p>No focusable content</p>"></iframe>
952
+ ```
953
+ `tabindex="-1"` removes the frame from the tab order, and nothing inside it is focusable either.
954
+
955
+ **Failed**
956
+ ```html
957
+ <iframe tabindex="-1" width="300" height="150" srcdoc="<a href='/x'>Link inside</a>"></iframe>
958
+ ```
959
+ The host frame is skipped by `tabindex="-1"`, but the link inside it is still reachable by Tab — the browser does not propagate the negative tabindex into the embedded document.
960
+
961
+ ## iframe-name-present
962
+
963
+ **Passed**
964
+ ```html
965
+ <iframe title="Video player" src="content.html"></iframe>
966
+ ```
967
+ The `title` attribute names the frame.
968
+
969
+ **Failed**
970
+ ```html
971
+ <iframe src="content.html"></iframe>
972
+ ```
973
+ No `title`, `aria-label`, or `aria-labelledby`.
974
+
975
+ ## iframe-title-unique
976
+
977
+ *Deprecated since 1.8.0 in favour of [`identical-iframes-same-purpose`](#identical-iframes-same-purpose): this rule reports `notApplicable` on every page, so it has no passing or failing example of its own.*
978
+
979
+ **Not applicable**
980
+ ```html
981
+ <iframe title="Video player" src="a.html"></iframe>
982
+ <iframe title="Video player" src="b.html"></iframe>
983
+ ```
984
+ Formerly a `fail` for the shared title. WCAG 4.1.2 does not require frame names to be unique; `identical-iframes-same-purpose` now raises this pair as `cantTell`, since the two frames share a name but embed different resources.
985
+
986
+ ## image-redundant-alt
987
+
988
+ **Flagged (cantTell)**
989
+ ```html
990
+ <a href="/"><img alt="Home" src="home.png">Home</a>
991
+ ```
992
+ The image's `alt` text ("Home") duplicates the adjacent visible link text word for word — a screen reader announces it twice.
993
+
994
+ **Not applicable**
995
+ ```html
996
+ <a href="/"><img alt="Home icon" src="home.png">Home</a>
997
+ ```
998
+ The `alt` text ("Home icon") differs from the adjacent text ("Home").
999
+
1000
+ ## img-alt-decorative
1001
+
1002
+ *Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.*
1003
+
1004
+ **Flagged (cantTell)**
1005
+ ```html
1006
+ <img src="quarterly-trend.png" alt="">
1007
+ ```
1008
+ Marked decorative with `alt=""`, but a filename like "quarterly-trend" suggests this might actually be an informative chart, not decoration.
1009
+
1010
+ **Not applicable**
1011
+ ```html
1012
+ <button aria-label="Close"><img src="x-icon.png" alt=""></button>
1013
+ ```
1014
+ The button's own `aria-label` already provides the accessible name; the icon's exclusion isn't this element's decorative question to review.
1015
+
1016
+ ## img-alt-present
1017
+
1018
+ **Passed**
1019
+ ```html
1020
+ <img src="image.png" alt="Green rectangle">
1021
+ ```
1022
+ A non-empty `alt` is present.
1023
+
1024
+ **Failed**
1025
+ ```html
1026
+ <img src="image.png">
1027
+ ```
1028
+ No `alt` attribute at all (an empty `alt=""` would instead pass as decorative).
1029
+
1030
+ ## img-alt-quality
1031
+
1032
+ *Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.*
1033
+
1034
+ **Flagged (cantTell)**
1035
+ ```html
1036
+ <img src="chart.png" alt="image">
1037
+ ```
1038
+ Non-empty alt is present, but "image" restates the element type instead of describing what the chart shows — worth a reviewer's judgment.
1039
+
1040
+ **Not applicable**
1041
+ ```html
1042
+ <img src="border.png" alt="">
1043
+ ```
1044
+ Empty alt marks the image decorative; that's `img-alt-decorative`'s question, not this rule's.
1045
+
1046
+ ## input-image-alt-decorative
1047
+
1048
+ *Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.*
1049
+
1050
+ **Flagged (cantTell)**
1051
+ ```html
1052
+ <input type="image" src="submit.png" alt="" title="Submit order">
1053
+ ```
1054
+ Empty alt is unusual on a functional control; a `title` still names it, but worth confirming the empty alt was deliberate.
1055
+
1056
+ **Not applicable**
1057
+ ```html
1058
+ <input type="image" src="submit.png" alt="Submit order">
1059
+ ```
1060
+ Non-empty alt is `input-image-alt-quality`'s question, not this rule's.
1061
+
1062
+ ## input-image-alt-present
1063
+
1064
+ **Passed**
1065
+ ```html
1066
+ <input type="image" src="search.png" alt="Search">
1067
+ ```
1068
+ A non-empty `alt` names the image button.
1069
+
1070
+ **Failed**
1071
+ ```html
1072
+ <input type="image" src="search.png">
1073
+ ```
1074
+ No `alt` attribute.
1075
+
1076
+ ## input-image-alt-quality
1077
+
1078
+ *Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.*
1079
+
1080
+ **Flagged (cantTell)**
1081
+ ```html
1082
+ <input type="image" src="submit.png" alt="Submit order">
1083
+ ```
1084
+ Non-empty alt is present on this image button, worth confirming "Submit order" matches what the button actually does. An image button named only by `aria-label`, `aria-labelledby` or `title` is asked about too.
1085
+
1086
+ **Not applicable**
1087
+ ```html
1088
+ <input type="image" src="submit.png" alt="" title="Submit order">
1089
+ ```
1090
+ Empty alt on a functional control is `input-image-alt-decorative`'s question, not this rule's.
1091
+
1092
+ ## label-in-name
1093
+
1094
+ **Passed**
1095
+ ```html
1096
+ <button aria-label="Save changes">Save</button>
1097
+ ```
1098
+ The accessible name ("Save changes") contains the visible text ("Save").
1099
+
1100
+ **Failed**
1101
+ ```html
1102
+ <button aria-label="Submit form">Save</button>
1103
+ ```
1104
+ The accessible name doesn't contain the visible label text at all.
1105
+
1106
+ ## label-title-only
1107
+
1108
+ **Flagged (cantTell)**
1109
+ ```html
1110
+ <input title="Name">
1111
+ ```
1112
+ The `title` attribute is the field's only naming mechanism — it shows as a tooltip, not a persistent visible label.
1113
+
1114
+ **Not applicable**
1115
+ ```html
1116
+ <label>Name <input title="Enter your full name"></label>
1117
+ ```
1118
+ A real `<label>` already names the field; `title` is just supplementary.
1119
+
1120
+ ## landmark-banner-is-top-level
1121
+
1122
+ **Flagged (cantTell)**
1123
+ ```html
1124
+ <div role="navigation">
1125
+ <div role="banner">Nested banner</div>
1126
+ </div>
1127
+ ```
1128
+ A banner landmark nested inside another landmark no longer functions as the page's single top-level header.
1129
+
1130
+ **Not applicable**
1131
+ ```html
1132
+ <header>Site header</header>
1133
+ ```
1134
+ The banner is not nested inside any other landmark.
1135
+
1136
+ ## landmark-complementary-is-top-level
1137
+
1138
+ **Flagged (cantTell)**
1139
+ ```html
1140
+ <nav aria-label="Section navigation">
1141
+ <div role="complementary">Related links</div>
1142
+ </nav>
1143
+ ```
1144
+ A complementary landmark nested inside a navigation landmark.
1145
+
1146
+ **Not applicable**
1147
+ ```html
1148
+ <aside>Related links</aside>
1149
+ ```
1150
+ The complementary landmark is not nested inside any other landmark.
1151
+
1152
+ ## landmark-contentinfo-is-top-level
1153
+
1154
+ **Flagged (cantTell)**
1155
+ ```html
1156
+ <div role="navigation">
1157
+ <div role="contentinfo">Nested contentinfo</div>
1158
+ </div>
1159
+ ```
1160
+ A contentinfo landmark nested inside another landmark no longer functions as the page's single top-level footer.
1161
+
1162
+ **Not applicable**
1163
+ ```html
1164
+ <footer>Site footer</footer>
1165
+ ```
1166
+ The contentinfo is not nested inside any other landmark.
1167
+
1168
+ ## landmark-main-is-top-level
1169
+
1170
+ **Flagged (cantTell)**
1171
+ ```html
1172
+ <div role="navigation">
1173
+ <main>Nested main</main>
1174
+ </div>
1175
+ ```
1176
+ The main landmark is nested inside a navigation landmark instead of being top-level.
1177
+
1178
+ **Not applicable**
1179
+ ```html
1180
+ <main>Content</main>
1181
+ ```
1182
+ The main landmark is not nested inside any other landmark.
1183
+
1184
+ ## landmark-no-duplicate-banner
1185
+
1186
+ **Flagged (cantTell)**
1187
+ ```html
1188
+ <body>
1189
+ <header>First</header>
1190
+ <header>Second</header>
1191
+ </body>
1192
+ ```
1193
+ Two top-level `<header>` elements both resolve to the banner role.
1194
+
1195
+ **Not applicable**
1196
+ ```html
1197
+ <body>
1198
+ <header>Site header</header>
1199
+ </body>
1200
+ ```
1201
+ Only one banner landmark exists.
1202
+
1203
+ ## landmark-no-duplicate-contentinfo
1204
+
1205
+ **Flagged (cantTell)**
1206
+ ```html
1207
+ <body>
1208
+ <footer>First</footer>
1209
+ <footer>Second</footer>
1210
+ </body>
1211
+ ```
1212
+ Two top-level `<footer>` elements both resolve to the contentinfo role.
1213
+
1214
+ **Not applicable**
1215
+ ```html
1216
+ <body>
1217
+ <footer>Site footer</footer>
1218
+ </body>
1219
+ ```
1220
+ Only one contentinfo landmark exists.
1221
+
1222
+ ## landmark-no-duplicate-main
1223
+
1224
+ **Flagged (cantTell)**
1225
+ ```html
1226
+ <body>
1227
+ <main>First</main>
1228
+ <main>Second</main>
1229
+ </body>
1230
+ ```
1231
+ Two `<main>` elements are both exposed to assistive technology as main landmarks.
1232
+
1233
+ **Not applicable**
1234
+ ```html
1235
+ <body>
1236
+ <main>Only one</main>
1237
+ </body>
1238
+ ```
1239
+ Only one main landmark exists.
1240
+
1241
+ ## landmark-one-main
1242
+
1243
+ **Flagged (cantTell)**
1244
+ ```html
1245
+ <body>
1246
+ <header>Site</header>
1247
+ <p>Primary content, no landmark.</p>
1248
+ </body>
1249
+ ```
1250
+ No `<main>` or `role="main"` anywhere on the page.
1251
+
1252
+ **Not applicable**
1253
+ ```html
1254
+ <body>
1255
+ <main>Primary content</main>
1256
+ </body>
1257
+ ```
1258
+ A `<main>` landmark exists, so there is nothing to flag.
1259
+
1260
+ ## landmark-unique
1261
+
1262
+ **Flagged (cantTell)**
1263
+ ```html
1264
+ <nav aria-label="Site">First</nav>
1265
+ <nav aria-label="Site">Second</nav>
1266
+ ```
1267
+ Two navigation landmarks share the identical accessible name, so a screen reader user can't tell them apart in the landmarks list.
1268
+
1269
+ **Not applicable**
1270
+ ```html
1271
+ <nav aria-label="Primary">Primary nav</nav>
1272
+ <nav aria-label="Footer">Footer nav</nav>
1273
+ ```
1274
+ Each navigation landmark has a distinct accessible name.
1275
+
1276
+ ## link-in-text-block
1277
+
1278
+ **Passed**
1279
+ ```html
1280
+ <p>Read <a href="#" style="text-decoration:underline">this underlined link</a> for more information.</p>
1281
+ ```
1282
+ The link is underlined, distinguishing it from surrounding text by more than color.
1283
+
1284
+ **Failed**
1285
+ ```html
1286
+ <p style="color:#222222;">Read <a href="#" style="text-decoration:none; color:#2a2a2a;">this link</a> for more information.</p>
1287
+ ```
1288
+ The only difference from surrounding text is a color shift with about 1.1:1 contrast, well under the 3:1 minimum, and no underline or weight/style change.
1289
+
1290
+ **Flagged (cantTell)**
1291
+ ```html
1292
+ <p style="color:#000000;">Read <a href="#" style="text-decoration:none; color:#d00000;">this link</a> for more information.</p>
1293
+ ```
1294
+ The link's color contrasts with the text by more than 3:1, but color is its only cue. Technique G183 also needs a non-color cue on hover and focus, which a person checks.
1295
+
1296
+ ## link-name-present
1297
+
1298
+ **Passed**
1299
+ ```html
1300
+ <a href="/x">Read more</a>
1301
+ ```
1302
+ The link's text content is its accessible name.
1303
+
1304
+ **Failed**
1305
+ ```html
1306
+ <a href="/x"></a>
1307
+ ```
1308
+ No text content, `aria-label`, or `aria-labelledby`.
1309
+
1310
+ ## link-name-quality
1311
+
1312
+ *A link with no accessible name at all is `link-name-present`'s concern, not this rule's.*
1313
+
1314
+ **Flagged (cantTell)**
1315
+ ```html
1316
+ <a href="/x">Click here</a>
1317
+ ```
1318
+ "Click here" is a known generic phrase that doesn't describe the link's destination out of context.
1319
+
1320
+ **Not applicable**
1321
+ ```html
1322
+ <a href="/x">Download the 2026 pricing guide</a>
1323
+ ```
1324
+ The link text is specific and descriptive.
1325
+
1326
+ ## list-children-valid
1327
+
1328
+ **Passed**
1329
+ ```html
1330
+ <ul>
1331
+ <li>Apple</li>
1332
+ <li>Banana</li>
1333
+ </ul>
1334
+ ```
1335
+ `<ul>`'s only direct children are `<li>`.
1336
+
1337
+ **Failed**
1338
+ ```html
1339
+ <ul>
1340
+ <li>Elderberry</li>
1341
+ <div>wrapper</div>
1342
+ </ul>
1343
+ ```
1344
+ A `<div>` is a direct child of `<ul>` alongside the `<li>`.
1345
+
1346
+ ## listbox-name-present
1347
+
1348
+ **Passed**
1349
+ ```html
1350
+ <ul role="listbox" tabindex="0" aria-label="Fruit"></ul>
1351
+ ```
1352
+ `aria-label` names the listbox.
1353
+
1354
+ **Failed**
1355
+ ```html
1356
+ <ul role="listbox" tabindex="0"></ul>
1357
+ ```
1358
+ No accessible name from any supported mechanism.
1359
+
1360
+ ## listitem-parent-valid
1361
+
1362
+ **Passed**
1363
+ ```html
1364
+ <ul>
1365
+ <li>Apple</li>
1366
+ </ul>
1367
+ ```
1368
+ `<li>` is inside a `<ul>`.
1369
+
1370
+ **Failed**
1371
+ ```html
1372
+ <div>
1373
+ <li>Date</li>
1374
+ </div>
1375
+ ```
1376
+ `<li>` has no `<ul>`/`<ol>`/`role="list"` ancestor.
1377
+
1378
+ ## manual-review
1379
+
1380
+ *This rule has no `notApplicable` branch at all — it returns `cantTell` unconditionally on every page, including an empty `<body>` — so there is only one example, not a pair.*
1381
+
1382
+ **Flagged (cantTell)**
1383
+ ```html
1384
+ <body>
1385
+ <p>Any page content.</p>
1386
+ </body>
1387
+ ```
1388
+ Keyboard operability and focus order always need a person driving the page; this rule states that need on every run.
1389
+
1390
+ ## media-alternative-transcript-evidence
1391
+
1392
+ **Not applicable**
1393
+ ```html
1394
+ <video src="talk.mp4" controls aria-describedby="t1"></video>
1395
+ <div id="t1">Transcript: Welcome to today's talk...</div>
1396
+ ```
1397
+ `aria-describedby` binds to text explicitly identified as a transcript — strong evidence.
1398
+
1399
+ **Flagged (cantTell)**
1400
+ ```html
1401
+ <video src="talk.mp4" controls></video>
1402
+ ```
1403
+ No transcript or text-alternative evidence was found near the media element; the engine can't confirm one doesn't exist elsewhere on the page, so this is conservative, not a fail.
1404
+
1405
+ ## menuitem-name-present
1406
+
1407
+ **Passed**
1408
+ ```html
1409
+ <div role="menuitem" tabindex="0">Cut</div>
1410
+ ```
1411
+ Visible text content is a valid name source for role="menuitem".
1412
+
1413
+ **Failed**
1414
+ ```html
1415
+ <div role="menuitem" tabindex="0"></div>
1416
+ ```
1417
+ No name and no content.
1418
+
1419
+ ## meta-refresh-no-exceptions
1420
+
1421
+ **Passed**
1422
+ ```html
1423
+ <meta http-equiv="refresh" content="0; URL=/x">
1424
+ ```
1425
+ A delay of exactly 0 is an immediate redirect; there's nothing to interrupt a user mid-read.
1426
+
1427
+ **Failed**
1428
+ ```html
1429
+ <meta http-equiv="refresh" content="30; URL=/x">
1430
+ ```
1431
+ At AAA, any positive delay fails, with no exemption for a long delay.
1432
+
1433
+ ## meta-refresh-timing-absent
1434
+
1435
+ **Passed**
1436
+ ```html
1437
+ <meta http-equiv="refresh" content="90000; URL=/x">
1438
+ ```
1439
+ A delay over 20 hours (72000s) is exempt under WCAG 2.2.1 Exception 3.
1440
+
1441
+ **Failed**
1442
+ ```html
1443
+ <meta http-equiv="refresh" content="30; URL=/x">
1444
+ ```
1445
+ A 30-second delay refreshes the page on a timer the user cannot pause, stop, or extend.
1446
+
1447
+ ## meta-viewport-large
1448
+
1449
+ **Flagged (cantTell)**
1450
+ ```html
1451
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
1452
+ ```
1453
+ `user-scalable=no` blocks zooming past whatever scale the page loads at, short of the AAA 500% best-practice target.
1454
+
1455
+ **Not applicable**
1456
+ ```html
1457
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5">
1458
+ ```
1459
+ `maximum-scale=5` (500%) already meets the best-practice ceiling this rule checks toward.
1460
+
1461
+ ## meta-viewport-zoom-enabled
1462
+
1463
+ **Passed**
1464
+ ```html
1465
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5">
1466
+ ```
1467
+ `maximum-scale` of 5 (500%) still allows zooming well past the 200% WCAG 1.4.4 requires.
1468
+
1469
+ **Failed**
1470
+ ```html
1471
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
1472
+ ```
1473
+ `user-scalable=no` disables pinch-zoom entirely.
1474
+
1475
+ ## meter-name-present
1476
+
1477
+ **Passed**
1478
+ ```html
1479
+ <div role="meter" aria-valuenow="80" aria-label="Disk usage"></div>
1480
+ ```
1481
+ `aria-label` names the meter.
1482
+
1483
+ **Failed**
1484
+ ```html
1485
+ <div role="meter" aria-valuenow="80"></div>
1486
+ ```
1487
+ No accessible name from any supported mechanism.
1488
+
1489
+ ## mouse-only-event-handlers
1490
+
1491
+ **Flagged (cantTell)**
1492
+ ```html
1493
+ <div onmouseover="show()" onmouseout="hide()">Hover-only tooltip trigger</div>
1494
+ ```
1495
+ `onmouseover`/`onmouseout` have no `onfocus`/`onblur` counterpart, so a keyboard user can never trigger this behavior.
1496
+
1497
+ **Flagged (cantTell)**
1498
+ ```html
1499
+ <div onmouseover="show()" onfocus="show()">Menu</div>
1500
+ ```
1501
+ The `onfocus` handler never runs: the `<div>` cannot take focus.
1502
+
1503
+ **Not applicable**
1504
+ ```html
1505
+ <div onmouseover="show()" onmouseout="hide()" onfocus="show()" onblur="hide()" tabindex="0">Hover + focus pair</div>
1506
+ ```
1507
+ Each mouse handler is paired with a keyboard-equivalent focus handler.
1508
+
1509
+ ## nested-interactive-controls-absent
1510
+
1511
+ **Passed**
1512
+ ```html
1513
+ <button>Click me</button>
1514
+ ```
1515
+ No nested interactive control.
1516
+
1517
+ **Failed**
1518
+ ```html
1519
+ <button>
1520
+ <input type="checkbox"> Accept
1521
+ </button>
1522
+ ```
1523
+ A `<button>` contains a nested `<input type="checkbox">`, another focusable control.
1524
+
1525
+ ## no-autoplay-audio
1526
+
1527
+ **Flagged (cantTell)**
1528
+ ```html
1529
+ <audio autoplay src="x.mp3"></audio>
1530
+ ```
1531
+ The audio autoplays unmuted with no `controls` attribute, giving the user no way to pause or stop it.
1532
+
1533
+ **Not applicable**
1534
+ ```html
1535
+ <audio autoplay controls src="x.mp3"></audio>
1536
+ ```
1537
+ Native `controls` gives the user a pause/volume mechanism.
1538
+
1539
+ **Flagged (cantTell)**
1540
+ ```html
1541
+ <embed src="welcome.mp3">
1542
+ ```
1543
+ An `<embed>` or `<object>` that loads sound or video, and any `<bgsound>`, may play as soon as the page loads, and it has no `controls` or `muted` to read. One with `autostart="false"` (or a `<param>` saying so) is not asked about.
1544
+
1545
+ ## object-text-alternative-present
1546
+
1547
+ **Passed**
1548
+ ```html
1549
+ <object data="chart.svg" type="image/svg+xml">Sales chart for 2024</object>
1550
+ ```
1551
+ Fallback content between the tags counts as a text alternative.
1552
+
1553
+ **Failed**
1554
+ ```html
1555
+ <object data="chart.svg" type="image/svg+xml"></object>
1556
+ ```
1557
+ No fallback content and no accessible name.
1558
+
1559
+ ## object-text-alternative-quality
1560
+
1561
+ *Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.*
1562
+
1563
+ **Flagged (cantTell)**
1564
+ ```html
1565
+ <object data="brochure.pdf" type="application/pdf">Product brochure (PDF)</object>
1566
+ ```
1567
+ Fallback text is present — worth confirming "Product brochure (PDF)" is genuinely equivalent to the embedded document, not just a filler caption.
1568
+
1569
+ **Not applicable**
1570
+ ```html
1571
+ <object data="brochure.pdf" type="application/pdf"></object>
1572
+ ```
1573
+ No fallback content, ARIA name, or title exists to review; that's `object-text-alternative-present`'s failure instead.
1574
+
1575
+ ## option-name-present
1576
+
1577
+ **Passed**
1578
+ ```html
1579
+ <div role="option" tabindex="0">Apple</div>
1580
+ ```
1581
+ Visible text content is a valid name source for role="option".
1582
+
1583
+ **Failed**
1584
+ ```html
1585
+ <div role="option" tabindex="0"></div>
1586
+ ```
1587
+ No name and no content.
1588
+
1589
+ ## p-as-heading
1590
+
1591
+ **Flagged (cantTell)**
1592
+ ```html
1593
+ <style>.fakeHeading { font-weight: bold; font-size: 22px; }</style>
1594
+ <p class="fakeHeading">Section Title</p>
1595
+ ```
1596
+ Bold, heading-sized, and short — visually a heading, but marked up as a plain `<p>` with no heading semantics.
1597
+
1598
+ **Not applicable**
1599
+ ```html
1600
+ <p>Just a normal paragraph of body text that is not bold at all.</p>
1601
+ ```
1602
+ Normal weight and size; nothing visually suggests a heading.
1603
+
1604
+ **Flagged (cantTell)**
1605
+ ```html
1606
+ <div style="font-size: 24px"><span style="font-weight: bold">Our team</span></div>
1607
+ ```
1608
+ A `<div>` holding only text is checked like a `<p>`, and the bold can come from a styled `<span>` inside it. A `<div>` that holds other blocks, and text inside a button, label or table header, is not asked about.
1609
+
1610
+ ## page-has-heading-one
1611
+
1612
+ **Flagged (cantTell)**
1613
+ ```html
1614
+ <body>
1615
+ <h2>Section</h2>
1616
+ <p>No top-level heading anywhere on the page.</p>
1617
+ </body>
1618
+ ```
1619
+ No `<h1>` or `role="heading" aria-level="1"` exists.
1620
+
1621
+ **Not applicable**
1622
+ ```html
1623
+ <body>
1624
+ <h1>Page title</h1>
1625
+ </body>
1626
+ ```
1627
+ A level-one heading exists, so there is nothing to flag.
1628
+
1629
+ ## page-title-patterns
1630
+
1631
+ **Flagged (cantTell)**
1632
+ ```html
1633
+ <title>Home</title>
1634
+ ```
1635
+ "Home" is one of the conservative generic-title signals this rule reviews.
1636
+
1637
+ **Not applicable**
1638
+ ```html
1639
+ <title>Quarterly Sales Report — Q3 2026</title>
1640
+ ```
1641
+ The title is specific to the page's content, matching none of the low-descriptiveness patterns.
1642
+
1643
+ ## page-title-present
1644
+
1645
+ **Passed**
1646
+ ```html
1647
+ <html>
1648
+ <head><title>Quarterly report</title></head>
1649
+ <body>Hi</body>
1650
+ </html>
1651
+ ```
1652
+ The document has a non-empty `<title>`.
1653
+
1654
+ **Failed**
1655
+ ```html
1656
+ <html>
1657
+ <head></head>
1658
+ <body>Hi</body>
1659
+ </html>
1660
+ ```
1661
+ No `<title>` element anywhere in the document.
1662
+
1663
+ ## password-paste-enabled
1664
+
1665
+ *This rule never fails — every case is either raised for review or left alone. An `addEventListener`-attached paste handler leaves no trace in markup, so it can't be demonstrated in a static example.*
1666
+
1667
+ **Flagged (cantTell)**
1668
+ ```html
1669
+ <label for="p">Password</label>
1670
+ <input type="password" id="p" onpaste="return false">
1671
+ ```
1672
+ The inline handler cancels paste outright, blocking a password manager or a copied one-time code.
1673
+
1674
+ **Not applicable**
1675
+ ```html
1676
+ <label for="p">Password</label>
1677
+ <input type="password" id="p" autocomplete="current-password">
1678
+ ```
1679
+ No paste handler is attached at all.
1680
+
1681
+ ## presentation-role-conflict
1682
+
1683
+ **Flagged (cantTell)**
1684
+ ```html
1685
+ <div role="presentation" aria-label="Conflicting">Content</div>
1686
+ ```
1687
+ `role="presentation"` asks assistive technology to skip this element, but `aria-label` supplies a name — per WAI-ARIA conflict resolution, the global attribute wins and the presentation role is dropped, so the element (and its name) come back into the tree unexpectedly.
1688
+
1689
+ **Not applicable**
1690
+ ```html
1691
+ <div role="presentation">Decorative</div>
1692
+ ```
1693
+ No conflicting naming attribute is present alongside the presentation role.
1694
+
1695
+ ## presentational-children-focusable-absent
1696
+
1697
+ **Passed**
1698
+ ```html
1699
+ <button>Save</button>
1700
+ ```
1701
+ No presentational content that also happens to be focusable.
1702
+
1703
+ **Failed**
1704
+ ```html
1705
+ <button>
1706
+ Save
1707
+ <span role="button" aria-label="save options" aria-expanded="false" tabindex="0">▼</span>
1708
+ </button>
1709
+ ```
1710
+ The nested `<span>` carries its own interactive role and tab stop inside the button's presentational content.
1711
+
1712
+ ## progressbar-name-present
1713
+
1714
+ **Passed**
1715
+ ```html
1716
+ <div role="progressbar" aria-valuenow="50" aria-label="Upload progress"></div>
1717
+ ```
1718
+ `aria-label` names the progress bar.
1719
+
1720
+ **Failed**
1721
+ ```html
1722
+ <div role="progressbar" aria-valuenow="50"></div>
1723
+ ```
1724
+ No accessible name from any supported mechanism.
1725
+
1726
+ ## region
1727
+
1728
+ **Flagged (cantTell)**
1729
+ ```html
1730
+ <body>
1731
+ <p>Stray paragraph, not inside any landmark.</p>
1732
+ </body>
1733
+ ```
1734
+ This paragraph sits directly under `<body>`, outside every landmark.
1735
+
1736
+ **Not applicable**
1737
+ ```html
1738
+ <body>
1739
+ <main>
1740
+ <p>Everything lives inside a landmark.</p>
1741
+ </main>
1742
+ </body>
1743
+ ```
1744
+ All page content is contained within a landmark region.
1745
+
1746
+ ## role-img-text-alternative-present
1747
+
1748
+ **Passed**
1749
+ ```html
1750
+ <span role="img" aria-label="Notifications"></span>
1751
+ ```
1752
+ `aria-label` provides the text alternative.
1753
+
1754
+ **Failed**
1755
+ ```html
1756
+ <span role="img">notifications</span>
1757
+ ```
1758
+ Text content is not a valid name source for an element with an explicit role="img".
1759
+
1760
+ ## scope-attr-valid
1761
+
1762
+ **Flagged (cantTell)**
1763
+ ```html
1764
+ <table><tr><th scope="column">Name</th></tr></table>
1765
+ ```
1766
+ "column" is not a valid `scope` value (the correct token is "col").
1767
+
1768
+ **Not applicable**
1769
+ ```html
1770
+ <table><tr><th scope="col">Name</th></tr></table>
1771
+ ```
1772
+ "col" is a valid `scope` value.
1773
+
1774
+ ## scrollable-region-focusable
1775
+
1776
+ **Flagged (cantTell)**
1777
+ ```html
1778
+ <style>.scrollY { overflow-y: auto; height: 100px; }</style>
1779
+ <div class="scrollY">Plain long text content with no interactive children at all.</div>
1780
+ ```
1781
+ The region can scroll, but it has no `tabindex` of its own and contains no focusable descendant, so a keyboard user can never reach it to scroll it.
1782
+
1783
+ **Not applicable**
1784
+ ```html
1785
+ <style>.scrollY { overflow-y: auto; height: 100px; }</style>
1786
+ <div class="scrollY" tabindex="0">Long content, but the region itself is focusable.</div>
1787
+ ```
1788
+ `tabindex="0"` makes the scrollable region itself reachable by keyboard.
1789
+
1790
+ ## searchbox-name-present
1791
+
1792
+ **Passed**
1793
+ ```html
1794
+ <div role="searchbox" tabindex="0" aria-label="Search the site"></div>
1795
+ ```
1796
+ `aria-label` names the searchbox.
1797
+
1798
+ **Failed**
1799
+ ```html
1800
+ <div role="searchbox" tabindex="0"></div>
1801
+ ```
1802
+ No accessible name from any supported mechanism.
1803
+
1804
+ ## server-side-image-map-absent
1805
+
1806
+ **Passed**
1807
+ ```html
1808
+ <img src="image.png" usemap="#nav" alt="Site map">
1809
+ <map name="nav">
1810
+ <area href="/x" shape="rect" coords="0,0,10,10" alt="Go">
1811
+ </map>
1812
+ ```
1813
+ A client-side image map has no `ismap` attribute, which satisfies the rule outright. (An `<img ismap>` outside a link does nothing and makes the rule not applicable.)
1814
+
1815
+ **Flagged (cantTell)**
1816
+ ```html
1817
+ <a href="/map-handler">
1818
+ <img src="image.png" ismap alt="Site map">
1819
+ </a>
1820
+ ```
1821
+ `ismap` depends on the browser sending click coordinates to the server, so the map's regions cannot be reached from the keyboard; whether the page offers the same destinations as links is for a person to check. This rule never fails.
1822
+
1823
+ ## skip-link
1824
+
1825
+ **Flagged (cantTell)**
1826
+ ```html
1827
+ <a href="#missing">Skip to main content</a>
1828
+ ```
1829
+ The link's fragment target doesn't exist anywhere on the page, so activating it goes nowhere.
1830
+
1831
+ **Flagged (cantTell)**
1832
+ ```html
1833
+ <a href="#contenu">Aller au contenu</a>
1834
+ ```
1835
+ French skip-link wording is recognised as well, and the target is missing.
1836
+
1837
+ **Not applicable**
1838
+ ```html
1839
+ <a href="#target">Skip to content</a>
1840
+ <div id="target">Target content</div>
1841
+ ```
1842
+ The fragment target exists and is usable.
1843
+
1844
+ ## slider-name-present
1845
+
1846
+ **Passed**
1847
+ ```html
1848
+ <input type="range" aria-label="Volume">
1849
+ ```
1850
+ `aria-label` names the native range input acting as a slider.
1851
+
1852
+ **Failed**
1853
+ ```html
1854
+ <div role="slider" tabindex="0" aria-valuenow="5" aria-valuemin="0" aria-valuemax="10"></div>
1855
+ ```
1856
+ No accessible name from any supported mechanism.
1857
+
1858
+ ## spinbutton-name-present
1859
+
1860
+ **Passed**
1861
+ ```html
1862
+ <div role="spinbutton" tabindex="0" aria-label="Quantity"></div>
1863
+ ```
1864
+ `aria-label` names the spinbutton.
1865
+
1866
+ **Failed**
1867
+ ```html
1868
+ <div role="spinbutton" tabindex="0"></div>
1869
+ ```
1870
+ No accessible name from any supported mechanism.
1871
+
1872
+ ## summary-name-present
1873
+
1874
+ **Passed**
1875
+ ```html
1876
+ <details>
1877
+ <summary>More details</summary>
1878
+ <p>Body</p>
1879
+ </details>
1880
+ ```
1881
+ Visible text content names the `<summary>`.
1882
+
1883
+ **Failed**
1884
+ ```html
1885
+ <details>
1886
+ <summary></summary>
1887
+ <p>Body</p>
1888
+ </details>
1889
+ ```
1890
+ No text content and no accessible name.
1891
+
1892
+ ## svg-image-text-alternative-present
1893
+
1894
+ **Passed**
1895
+ ```html
1896
+ <svg width="40" height="24">
1897
+ <image href="logo.png"><title>Logo</title></image>
1898
+ </svg>
1899
+ ```
1900
+ A `<title>` inside the SVG `<image>` provides the alternative.
1901
+
1902
+ **Failed**
1903
+ ```html
1904
+ <svg width="40" height="24" aria-label="container">
1905
+ <image href="logo.png"></image>
1906
+ </svg>
1907
+ ```
1908
+ The `<image>` itself has no `<title>`/`<desc>`/ARIA name (the outer `<svg>`'s label doesn't transfer to it).
1909
+
1910
+ ## svg-text-alternative-present
1911
+
1912
+ **Passed**
1913
+ ```html
1914
+ <svg role="img"><title>Close</title></svg>
1915
+ ```
1916
+ `<title>` provides the accessible name for an SVG with an explicit img role.
1917
+
1918
+ **Failed**
1919
+ ```html
1920
+ <svg role="img"></svg>
1921
+ ```
1922
+ No `<title>`, `<desc>`, or ARIA name.
1923
+
1924
+ ## svg-text-alternative-quality
1925
+
1926
+ *Manual; flags every applicable element with a detected text alternative for review, not only the ones that look wrong.*
1927
+
1928
+ **Flagged (cantTell)**
1929
+ ```html
1930
+ <svg role="img" viewBox="0 0 24 24"><title>Growth chart</title><path d="M0 0h24v24H0z"/></svg>
1931
+ ```
1932
+ A `<title>` is present — worth confirming "Growth chart" describes what the chart actually shows, not just that it's a chart.
1933
+
1934
+ **Not applicable**
1935
+ ```html
1936
+ <svg role="img" viewBox="0 0 24 24"><path d="M0 0h24v24H0z"/></svg>
1937
+ ```
1938
+ No `<title>`/`<desc>` text, aria-label, or resolved aria-labelledby exists to review; that's `svg-text-alternative-present`'s failure instead.
1939
+
1940
+ ## tab-name-present
1941
+
1942
+ **Passed**
1943
+ ```html
1944
+ <div role="tab" tabindex="0">Apple</div>
1945
+ ```
1946
+ Visible text content names the tab.
1947
+
1948
+ **Failed**
1949
+ ```html
1950
+ <div role="tab" tabindex="0"></div>
1951
+ ```
1952
+ No name and no content.
1953
+
1954
+ ## tabindex
1955
+
1956
+ **Flagged (cantTell)**
1957
+ ```html
1958
+ <div tabindex="3">Reordered</div>
1959
+ ```
1960
+ A positive `tabindex` pulls this element out of the document's natural tab order and ahead of everything else, which usually surprises keyboard users.
1961
+
1962
+ **Not applicable**
1963
+ ```html
1964
+ <div tabindex="0">Focusable</div>
1965
+ ```
1966
+ `tabindex="0"` places the element in the natural tab order without reordering anything.
1967
+
1968
+ ## table-duplicate-name
1969
+
1970
+ **Flagged (cantTell)**
1971
+ ```html
1972
+ <table summary="Sales data">
1973
+ <caption>Sales data</caption>
1974
+ <tr><td>1</td></tr>
1975
+ </table>
1976
+ ```
1977
+ The `summary` attribute repeats the caption text word for word instead of adding anything new.
1978
+
1979
+ **Not applicable**
1980
+ ```html
1981
+ <table summary="Data collected in fiscal year 2025">
1982
+ <caption>Sales data</caption>
1983
+ <tr><td>1</td></tr>
1984
+ </table>
1985
+ ```
1986
+ `summary` provides information the caption doesn't.
1987
+
1988
+ ## table-fake-caption
1989
+
1990
+ **Flagged (cantTell)**
1991
+ ```html
1992
+ <table>
1993
+ <tr><td>Monthly Sales Report</td></tr>
1994
+ <tr><td>Jan</td><td>100</td></tr>
1995
+ <tr><td>Feb</td><td>200</td></tr>
1996
+ </table>
1997
+ ```
1998
+ A lone cell sits alone in the first row above rows that all have multiple cells, and there's no real `<caption>` — it looks like a caption pretending to be a data cell.
1999
+
2000
+ **Not applicable**
2001
+ ```html
2002
+ <table>
2003
+ <caption>Monthly Sales Report</caption>
2004
+ <tr><td>Title cell</td></tr>
2005
+ <tr><td>Jan</td><td>100</td></tr>
2006
+ </table>
2007
+ ```
2008
+ The table has a real `<caption>` element.
2009
+
2010
+ ## table-headers-attr-valid
2011
+
2012
+ **Passed**
2013
+ ```html
2014
+ <table>
2015
+ <tr><th id="h1">Name</th></tr>
2016
+ <tr><td headers="h1">Apple</td></tr>
2017
+ </table>
2018
+ ```
2019
+ `headers="h1"` resolves to a real cell in the same table.
2020
+
2021
+ **Failed**
2022
+ ```html
2023
+ <table>
2024
+ <tr><th id="h2">Name</th></tr>
2025
+ <tr><td headers="missing">Banana</td></tr>
2026
+ </table>
2027
+ ```
2028
+ `headers` references an id that doesn't exist in the table.
2029
+
2030
+ ## table-th-has-data-cells
2031
+
2032
+ **Passed**
2033
+ ```html
2034
+ <table>
2035
+ <tr><th>Name</th></tr>
2036
+ <tr><td>Apple</td></tr>
2037
+ </table>
2038
+ ```
2039
+ The table has at least one `<td>` for the `<th>` to head.
2040
+
2041
+ **Failed**
2042
+ ```html
2043
+ <table>
2044
+ <tr><th>Name</th></tr>
2045
+ </table>
2046
+ ```
2047
+ The table has a `<th>` but zero `<td>` anywhere.
2048
+
2049
+ ## target-size-minimum
2050
+
2051
+ *Confirmed against the engine's own geometry-testing harness (`patchTargetSizeEnv`), which encodes element size/position as a `data-rect` attribute rather than real CSS layout (JSDOM doesn't render). The CSS below is what a real browser would need to reproduce the same result; it isn't lifted from the fixture's own markup.*
2052
+
2053
+ **Passed**
2054
+ ```html
2055
+ <button style="width:24px; height:24px;">+</button>
2056
+ ```
2057
+ Meets the 24×24 CSS pixel minimum on its own.
2058
+
2059
+ **Failed**
2060
+ ```html
2061
+ <button style="width:10px; height:10px;">A</button>
2062
+ <button style="width:10px; height:10px; margin-left:5px;">B</button>
2063
+ ```
2064
+ Both targets are under 24×24 and closer together than the spacing exception allows.
2065
+
2066
+ ## td-has-header
2067
+
2068
+ **Passed**
2069
+ ```html
2070
+ <table>
2071
+ <tr><th></th><th>Q1</th><th>Q2</th><th>Q3</th></tr>
2072
+ <tr><th>North</th><td>1</td><td>2</td><td>3</td></tr>
2073
+ <tr><th>South</th><td>4</td><td>5</td><td>6</td></tr>
2074
+ <tr><th>East</th><td>7</td><td>8</td><td>9</td></tr>
2075
+ </table>
2076
+ ```
2077
+ Every `<td>` has both an implicit row header (`<th>` earlier in its row) and column header (`<th>` earlier in its column). The 4x4 minimum size isn't optional — this rule is inapplicable to smaller tables.
2078
+
2079
+ **Failed**
2080
+ ```html
2081
+ <table>
2082
+ <tr><td>a</td><td>b</td><td>c</td><td>d</td></tr>
2083
+ <tr><td>e</td><td>f</td><td>g</td><td>h</td></tr>
2084
+ <tr><td>i</td><td>j</td><td>k</td><td>l</td></tr>
2085
+ <tr><td>m</td><td>n</td><td>o</td><td>p</td></tr>
2086
+ </table>
2087
+ ```
2088
+ A 4x4 table with no `<th>` anywhere — no cell has a row, column, or `headers`-attribute association.
2089
+
2090
+ ## text-spacing-content-loss
2091
+
2092
+ **Passed (in a browser)**
2093
+ ```html
2094
+ <p>Opening hours today, and the rest of the week from nine to five.</p>
2095
+ ```
2096
+ The paragraph has no fixed size, so the text grows with the spacing and nothing is lost.
2097
+
2098
+ **Failed (in a browser)**
2099
+ ```html
2100
+ <style>
2101
+ .hours { width: 19ch; height: 20px; overflow: hidden; }
2102
+ </style>
2103
+ <div class="hours">Opening hours today</div>
2104
+ ```
2105
+ With line height 1.5, letter spacing 0.12em and word spacing 0.16em, the text wraps to a second line that the box cuts off. In jsdom, which has no layout, the rule reads only the style sheets and asks about spacing forced with `!important`.
2106
+
2107
+ ## textbox-name-present
2108
+
2109
+ **Passed**
2110
+ ```html
2111
+ <div role="textbox" tabindex="0" aria-label="Search the site"></div>
2112
+ ```
2113
+ `aria-label` names the textbox.
2114
+
2115
+ **Failed**
2116
+ ```html
2117
+ <div role="textbox" tabindex="0"></div>
2118
+ ```
2119
+ No accessible name from any supported mechanism.
2120
+
2121
+ ## tooltip-name-present
2122
+
2123
+ **Passed**
2124
+ ```html
2125
+ <div role="tooltip">Click to save your changes</div>
2126
+ ```
2127
+ Visible text content names the tooltip.
2128
+
2129
+ **Failed**
2130
+ ```html
2131
+ <div role="tooltip"></div>
2132
+ ```
2133
+ No text content and no accessible name.
2134
+
2135
+ ## treeitem-name-present
2136
+
2137
+ **Passed**
2138
+ ```html
2139
+ <div role="treeitem" tabindex="0">Apple</div>
2140
+ ```
2141
+ Visible text content names the tree item.
2142
+
2143
+ **Failed**
2144
+ ```html
2145
+ <div role="treeitem" tabindex="0"></div>
2146
+ ```
2147
+ No name and no content.
2148
+
2149
+ ## valid-lang
2150
+
2151
+ **Passed**
2152
+ ```html
2153
+ <p lang="fr">Bonjour</p>
2154
+ ```
2155
+ "fr" is a syntactically valid BCP 47 language tag.
2156
+
2157
+ **Failed**
2158
+ ```html
2159
+ <p lang="xyz123!!">???</p>
2160
+ ```
2161
+ The value isn't valid BCP 47 syntax.
2162
+
2163
+ ## video-caption
2164
+
2165
+ **Flagged (cantTell)**
2166
+ ```html
2167
+ <video src="x.mp4"></video>
2168
+ ```
2169
+ No `<track>` element at all, so no evidence of captions. A video whose only track is `kind="subtitles"` is asked about too, since subtitles may translate the dialogue without the sound information captions carry.
2170
+
2171
+ **Not applicable**
2172
+ ```html
2173
+ <video src="x.mp4"><track kind="captions" src="cap.vtt"></video>
2174
+ ```
2175
+ A `kind="captions"` track with a real `src` is present.
2176
+
2177
+ ## video-poster-text-alternative-present
2178
+
2179
+ **Passed**
2180
+ ```html
2181
+ <video poster="poster.png" aria-label="Product demo"></video>
2182
+ ```
2183
+ `aria-label` names the video element that carries the poster image.
2184
+
2185
+ **Failed**
2186
+ ```html
2187
+ <video poster="poster.png"></video>
2188
+ ```
2189
+ No accessible name and no fallback; a poster attribute alone isn't a text alternative.