@surea11y/core 1.5.0 → 1.7.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.
Files changed (157) hide show
  1. package/CHANGELOG.md +240 -149
  2. package/README.md +51 -44
  3. package/docs/ACT_RULE_MAPPING.md +245 -0
  4. package/docs/API_STABILITY.md +53 -5
  5. package/docs/BINDING_AUTHORS_GUIDE.md +106 -4
  6. package/docs/DESIGN_CHALLENGES.md +367 -0
  7. package/docs/EARL.md +100 -0
  8. package/docs/ENGINE_OPTIONS.md +42 -4
  9. package/docs/I18N.md +4 -4
  10. package/docs/INTEGRATION.md +4 -2
  11. package/docs/LIMITATIONS.md +9 -5
  12. package/docs/OUTPUT_SCHEMA.md +44 -6
  13. package/docs/POLICY.md +1 -1
  14. package/docs/REPORT.md +1 -1
  15. package/docs/RULE_AUTHORING.md +63 -36
  16. package/docs/RULE_CATALOG.md +1928 -169
  17. package/docs/RULE_HELPERS.md +333 -0
  18. package/docs/RULE_TAXONOMY.md +27 -6
  19. package/docs/SARIF.md +21 -2
  20. package/docs/TROUBLESHOOTING.md +2 -2
  21. package/docs/WCAG_CONFORMANCE.md +34 -10
  22. package/package.json +11 -9
  23. package/src/baseline.js +3 -3
  24. package/src/checks/automatic/area-alt-present.js +2 -2
  25. package/src/checks/automatic/aria-allowed-attr.js +74 -10
  26. package/src/checks/automatic/aria-allowed-role.js +34 -25
  27. package/src/checks/automatic/aria-braille-equivalent.js +21 -13
  28. package/src/checks/automatic/aria-conditional-attr.js +22 -15
  29. package/src/checks/automatic/aria-deprecated-role.js +13 -1
  30. package/src/checks/automatic/aria-hidden-body.js +3 -3
  31. package/src/checks/automatic/aria-hidden-focus.js +5 -5
  32. package/src/checks/automatic/aria-prohibited-attr.js +23 -18
  33. package/src/checks/automatic/aria-prohibited-children.js +136 -43
  34. package/src/checks/automatic/aria-required-attr.js +119 -24
  35. package/src/checks/automatic/aria-required-children.js +54 -30
  36. package/src/checks/automatic/aria-required-parent.js +93 -15
  37. package/src/checks/automatic/aria-role-name-present.js +37 -23
  38. package/src/checks/automatic/aria-roles-valid.js +52 -21
  39. package/src/checks/automatic/aria-valid-attr-value.js +89 -33
  40. package/src/checks/automatic/aria-valid-attr.js +15 -10
  41. package/src/checks/automatic/autocomplete-valid.js +2 -2
  42. package/src/checks/automatic/avoid-inline-spacing.js +133 -6
  43. package/src/checks/automatic/binary-control-name-present.js +27 -5
  44. package/src/checks/automatic/button-name-present.js +92 -6
  45. package/src/checks/automatic/combobox-name-present.js +26 -6
  46. package/src/checks/automatic/contrast-computable.js +42 -0
  47. package/src/checks/automatic/contrast-enhanced.js +33 -1
  48. package/src/checks/automatic/contrast-minimum.js +33 -1
  49. package/src/checks/automatic/css-orientation-lock.js +138 -24
  50. package/src/checks/automatic/definition-list-children-valid.js +7 -8
  51. package/src/checks/automatic/deprecated-elements-not-used.js +1 -1
  52. package/src/checks/automatic/dialog-name-present.js +20 -2
  53. package/src/checks/automatic/duplicate-id-aria.js +10 -3
  54. package/src/checks/automatic/duplicate-id.js +203 -0
  55. package/src/checks/automatic/embed-text-alternative-present.js +2 -2
  56. package/src/checks/automatic/form-control-programmatic-label-present.js +19 -2
  57. package/src/checks/automatic/form-control-single-label.js +10 -1
  58. package/src/checks/automatic/identical-iframes-same-purpose.js +229 -0
  59. package/src/checks/automatic/iframe-focusable-content.js +68 -7
  60. package/src/checks/automatic/iframe-name-present.js +37 -3
  61. package/src/checks/automatic/iframe-title-unique.js +1 -1
  62. package/src/checks/automatic/img-alt-present.js +12 -4
  63. package/src/checks/automatic/label-in-name.js +204 -68
  64. package/src/checks/automatic/link-in-text-block.js +285 -29
  65. package/src/checks/automatic/link-name-present.js +22 -1
  66. package/src/checks/automatic/list-children-valid.js +6 -6
  67. package/src/checks/automatic/listbox-name-present.js +28 -8
  68. package/src/checks/automatic/listitem-parent-valid.js +4 -4
  69. package/src/checks/automatic/menuitem-name-present.js +20 -2
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +25 -14
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +14 -7
  72. package/src/checks/automatic/meter-name-present.js +23 -4
  73. package/src/checks/automatic/nested-interactive-controls-absent.js +5 -5
  74. package/src/checks/automatic/option-name-present.js +23 -4
  75. package/src/checks/automatic/page-title-present.js +21 -3
  76. package/src/checks/automatic/presentational-children-focusable-absent.js +330 -0
  77. package/src/checks/automatic/progressbar-name-present.js +23 -4
  78. package/src/checks/automatic/{role-img-alt-present.js → role-img-text-alternative-present.js} +64 -16
  79. package/src/checks/automatic/searchbox-name-present.js +28 -8
  80. package/src/checks/automatic/server-side-image-map-absent.js +1 -1
  81. package/src/checks/automatic/slider-name-present.js +27 -6
  82. package/src/checks/automatic/spinbutton-name-present.js +28 -8
  83. package/src/checks/automatic/summary-name-present.js +18 -2
  84. package/src/checks/automatic/svg-image-text-alternative-present.js +1 -1
  85. package/src/checks/automatic/svg-text-alternative-present.js +13 -10
  86. package/src/checks/automatic/tab-name-present.js +21 -2
  87. package/src/checks/automatic/table-headers-attr-valid.js +43 -8
  88. package/src/checks/automatic/table-th-has-data-cells.js +61 -5
  89. package/src/checks/automatic/target-size-minimum.js +155 -58
  90. package/src/checks/automatic/td-has-header.js +24 -23
  91. package/src/checks/automatic/textbox-name-present.js +28 -8
  92. package/src/checks/automatic/tooltip-name-present.js +21 -2
  93. package/src/checks/automatic/treeitem-name-present.js +23 -4
  94. package/src/checks/automatic/valid-lang.js +92 -7
  95. package/src/checks/automatic/video-poster-text-alternative-present.js +1 -1
  96. package/src/checks/manual/accesskeys-manual.js +3 -3
  97. package/src/checks/manual/area-alt-decorative-manual.js +7 -0
  98. package/src/checks/manual/area-alt-quality-manual.js +6 -0
  99. package/src/checks/manual/aria-checked-state-mismatch-manual.js +5 -5
  100. package/src/checks/manual/aria-text-manual.js +4 -4
  101. package/src/checks/manual/bypass-blocks-present-manual.js +44 -26
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +8 -0
  103. package/src/checks/manual/css-focus-indicator-suppressed-manual.js +444 -0
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +8 -0
  105. package/src/checks/manual/empty-heading-manual.js +58 -11
  106. package/src/checks/manual/empty-table-header-manual.js +8 -8
  107. package/src/checks/manual/focus-order-semantics-manual.js +15 -15
  108. package/src/checks/manual/form-control-label-quality-manual.js +563 -0
  109. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -2
  110. package/src/checks/manual/heading-order-manual.js +3 -3
  111. package/src/checks/manual/heading-quality-manual.js +338 -0
  112. package/src/checks/manual/identical-links-same-purpose-manual.js +73 -12
  113. package/src/checks/manual/image-redundant-alt-manual.js +4 -4
  114. package/src/checks/manual/img-alt-decorative-manual.js +211 -52
  115. package/src/checks/manual/img-alt-quality-manual.js +7 -0
  116. package/src/checks/manual/input-image-alt-decorative-manual.js +8 -0
  117. package/src/checks/manual/input-image-alt-quality-manual.js +5 -0
  118. package/src/checks/manual/label-title-only-manual.js +4 -4
  119. package/src/checks/manual/landmark-banner-is-top-level-manual.js +7 -7
  120. package/src/checks/manual/landmark-complementary-is-top-level-manual.js +231 -0
  121. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +9 -9
  122. package/src/checks/manual/landmark-main-is-top-level-manual.js +6 -6
  123. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +6 -6
  124. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +6 -6
  125. package/src/checks/manual/landmark-no-duplicate-main-manual.js +2 -2
  126. package/src/checks/manual/landmark-one-main-manual.js +6 -6
  127. package/src/checks/manual/landmark-unique-manual.js +9 -9
  128. package/src/checks/manual/link-name-quality-manual.js +161 -32
  129. package/src/checks/manual/media-transcript-present-manual.js +2 -3
  130. package/src/checks/manual/meta-viewport-large-manual.js +2 -2
  131. package/src/checks/manual/mouse-only-event-handlers-manual.js +9 -9
  132. package/src/checks/manual/no-autoplay-audio-manual.js +3 -3
  133. package/src/checks/manual/object-text-alternative-quality-manual.js +8 -0
  134. package/src/checks/manual/p-as-heading-manual.js +4 -4
  135. package/src/checks/manual/page-has-heading-one-manual.js +6 -6
  136. package/src/checks/manual/page-title-patterns-manual.js +26 -3
  137. package/src/checks/manual/password-paste-enabled-manual.js +255 -0
  138. package/src/checks/manual/presentation-role-conflict-manual.js +55 -25
  139. package/src/checks/manual/region-manual.js +19 -19
  140. package/src/checks/manual/scope-attr-valid-manual.js +2 -2
  141. package/src/checks/manual/scrollable-region-focusable-manual.js +7 -7
  142. package/src/checks/manual/skip-link-manual.js +5 -5
  143. package/src/checks/manual/svg-text-alternative-quality-manual.js +9 -0
  144. package/src/checks/manual/tabindex-manual.js +2 -2
  145. package/src/checks/manual/table-duplicate-name-manual.js +2 -2
  146. package/src/checks/manual/table-fake-caption-manual.js +2 -2
  147. package/src/checks/manual/video-caption-manual.js +3 -3
  148. package/src/checks/manual-review.js +17 -1
  149. package/src/core.js +8880 -41883
  150. package/src/earl.js +144 -0
  151. package/src/report.js +2 -2
  152. package/src/sarif.js +22 -2
  153. package/surea11y.browser.js +10 -37882
  154. package/surea11y.i18n.de.js +2 -21
  155. package/surea11y.i18n.es.js +2 -21
  156. package/surea11y.i18n.fr.js +2 -21
  157. package/bin/surea11y-core.js +0 -20
@@ -1,180 +1,1939 @@
1
1
  # Rule catalog
2
2
 
3
- Generated from the compiled engine's own catalog (`getChecksCatalog()`/`getRulesCatalog()`) — run `node scripts/generate-rule-catalog.js` after `npm run build` to regenerate this file whenever rules change. Do not hand-edit.
3
+ Generated from the compiled engine's own catalog (`getChecksCatalog()`/`getRulesCatalog()`) and each rule's source header. Run `node scripts/generate-rule-catalog.js` after `npm run build` to regenerate this file whenever rules change. Do not hand-edit.
4
4
 
5
- **125 rules total: 76 automatic (WCAG-normative, can return `fail`), 49 manual (advisory/judgment-required, capped at `cantTell`). 101 carry at least one formal WCAG Success Criterion mapping.**
5
+ **133 rules total: 79 automatic (WCAG-normative, can return `fail`), 54 manual (advisory/judgment-required, capped at `cantTell`). 107 carry at least one formal WCAG Success Criterion mapping.**
6
6
 
7
- See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`severity` mean on a scan result, and [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md) for how these roll up to an SC-level conformance claim. For WCAG-facet-level coverage-gap tracking (which parts of an SC are and aren't automatable yet), see `coverage/coverage-report.md` instead — that one is organized by facet, this one by rule.
7
+ The tables below are an index; [rule reference](#rule-reference) carries each rule's description, what it applies to and what it expects.
8
8
 
9
- ## Automatic rules (76) — can return `fail`
9
+ See [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md) for what `type`/`confidence`/`severity` mean on a scan result, and [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md) for how these roll up to an SC-level conformance claim. For WCAG-facet-level coverage-gap tracking (which parts of an SC are and aren't automatable yet), see `coverage/coverage-report.md` instead: that one is organized by facet, this one by rule.
10
+
11
+ ## Automatic rules (79), can return `fail`
10
12
 
11
13
  | Rule ID | Title | WCAG SC | Level | Confidence | Default severity |
12
14
  |---|---|---|---|---|---|
13
- | `area-alt-present` | <area> must have an alt attribute | 1.1.1 | A | high | serious |
14
- | `aria-allowed-attr` | aria-* attributes must be permitted for the element’s role | 4.1.2 | A | medium | moderate |
15
- | `aria-allowed-role` | Explicit role must be permitted for its host element | 4.1.2 | A | high | moderate |
16
- | `aria-braille-equivalent` | aria-braillelabel/aria-brailleroledescription must have a non-braille equivalent | 4.1.2 | A | high | serious |
17
- | `aria-conditional-attr` | aria-errormessage requires aria-invalid to be set to a non-false value | 4.1.2 | A | high | serious |
18
- | `aria-deprecated-role` | role attribute should not use a deprecated or author-discouraged ARIA role | 4.1.2 | A | high | moderate |
19
- | `aria-hidden-body` | The document <body> must not be aria-hidden | 1.3.1, 4.1.2 | A | high | critical |
20
- | `aria-hidden-focus` | ARIA hidden elements must not be focusable | 2.4.7, 4.1.2 | AA | high | serious |
21
- | `aria-prohibited-attr` | ARIA naming attributes must not be used on roles that prohibit them | 4.1.2 | A | high | moderate |
22
- | `aria-prohibited-children` | Container roles must not own a child with a disallowed role | 4.1.2 | A | medium | moderate |
23
- | `aria-required-attr` | Roles with a required ARIA state/property must carry it | 4.1.2 | A | high | serious |
24
- | `aria-required-children` | Container roles must own at least one required child role | 4.1.2 | A | medium | moderate |
25
- | `aria-required-parent` | Roles requiring a specific context role must be in that context | 4.1.2 | A | medium | moderate |
26
- | `aria-role-name-present` | ARIA widget/container roles have an accessible name | 4.1.2 | A | high | serious |
27
- | `aria-roles-valid` | role attribute must be a valid, non-abstract ARIA role | 4.1.2 | A | high | serious |
28
- | `aria-valid-attr` | aria-* attributes must be real, defined ARIA attributes | 4.1.2 | A | high | serious |
29
- | `aria-valid-attr-value` | aria-* attribute values must match their declared type | 4.1.2 | A | high | serious |
30
- | `autocomplete-valid` | autocomplete attribute must be a valid autofill value | 1.3.5 | AA | high | moderate |
31
- | `avoid-inline-spacing` | Inline style must not force text spacing below the WCAG metric | 1.4.12 | AA | high | moderate |
32
- | `binary-control-name-present` | Binary controls have an accessible name | 4.1.2 | A | high | serious |
33
- | `button-name-present` | Buttons have an accessible name | 4.1.2 | A | high | serious |
34
- | `canvas-text-alternative-present` | <canvas> must provide a text alternative | 1.1.1 | A | high | serious |
35
- | `combobox-name-present` | Comboboxes have an accessible name | 4.1.2 | A | high | serious |
36
- | `contrast-computable` | Color contrast is computable for rendered text | 1.4.3, 1.4.6 | AAA | high | serious |
37
- | `contrast-enhanced` | Text meets enhanced color contrast (AAA) | 1.4.6 | AAA | high | serious |
38
- | `contrast-minimum` | Text meets minimum color contrast (AA) | 1.4.3 | AA | high | serious |
39
- | `css-orientation-lock` | CSS must not lock the page to a single orientation | 1.3.4 | AA | high | serious |
40
- | `definition-list-children-valid` | Description lists must be structured correctly | 1.3.1 | A | high | serious |
41
- | `deprecated-elements-not-used` | Obsolete non-stoppable elements (<blink>, <marquee>) must not be used | 2.2.2 | A | high | serious |
42
- | `dialog-name-present` | Dialogs have an accessible name | 4.1.2 | A | high | serious |
43
- | `dlitem-parent-valid` | Description-list items must be inside a description list | 1.3.1 | A | high | serious |
44
- | `duplicate-id-aria` | IDs referenced by ARIA must be unique | 4.1.2 | A | high | serious |
45
- | `embed-text-alternative-present` | <embed> must provide a text alternative | 1.1.1 | A | high | serious |
46
- | `form-control-programmatic-label-present` | Form controls must have a programmatic label | 1.3.1, 3.3.2, 4.1.2 | A | medium | serious |
47
- | `form-control-single-label` | Form controls must not have multiple labels | 3.3.2 | A | high | moderate |
48
- | `html-lang-attr-present` | Page language is declared | 3.1.1 | A | high | serious |
49
- | `html-xml-lang-mismatch` | lang and xml:lang must not disagree | 3.1.1 | A | high | serious |
50
- | `iframe-focusable-content` | Frames with tabindex="-1" must not contain focusable content | 2.1.1 | A | high | moderate |
51
- | `iframe-name-present` | Frames have an accessible name | 4.1.2 | A | high | serious |
52
- | `iframe-title-unique` | Frame titles must be unique | 4.1.2 | A | high | moderate |
53
- | `img-alt-present` | <img> must have an alt attribute | 1.1.1 | A | high | serious |
54
- | `input-image-alt-present` | <input type="image"> must have an alt attribute | 1.1.1 | A | high | serious |
55
- | `label-in-name` | Label in Name: accessible name contains visible text | 2.5.3 | A | high | serious |
56
- | `link-in-text-block` | Links in text blocks must be distinguishable from surrounding text without relying on color alone | 1.4.1 | A | high | serious |
57
- | `link-name-present` | Links have an accessible name | 4.1.2 | A | high | serious |
58
- | `list-children-valid` | Lists must only directly contain list items | 1.3.1 | A | high | serious |
59
- | `listbox-name-present` | Listboxes have an accessible name | 4.1.2 | A | high | serious |
60
- | `listitem-parent-valid` | List items must be inside a list container | 1.3.1 | A | high | serious |
61
- | `menuitem-name-present` | Menu items have an accessible name | 4.1.2 | A | high | serious |
62
- | `meta-refresh-no-exceptions` | Page must not use a meta refresh at all (AAA) | 2.2.4, 3.2.5 | AAA | high | moderate |
63
- | `meta-refresh-timing-absent` | Page must not use a timed meta refresh | 2.2.1 | A | high | serious |
64
- | `meta-viewport-zoom-enabled` | Viewport meta tag must not disable zoom | 1.4.4 | AA | high | serious |
65
- | `meter-name-present` | Meters have an accessible name | 1.1.1 | A | high | serious |
66
- | `nested-interactive-controls-absent` | Interactive controls must not be nested | 4.1.2 | A | high | serious |
67
- | `object-text-alternative-present` | <object> must provide a text alternative | 1.1.1 | A | high | serious |
68
- | `option-name-present` | Options have an accessible name | 4.1.2 | A | high | serious |
69
- | `page-title-present` | Page has a non-empty title | 2.4.2 | A | high | serious |
70
- | `progressbar-name-present` | Progress bars have an accessible name | 1.1.1 | A | high | serious |
71
- | `role-img-text-alternative-present` | [role="img"] must have an accessible text alternative | 1.1.1 | A | high | serious |
72
- | `searchbox-name-present` | Searchboxes have an accessible name | 4.1.2 | A | high | serious |
73
- | `server-side-image-map-absent` | Images must not use a server-side image map | 2.1.1 | A | high | serious |
74
- | `slider-name-present` | Sliders have an accessible name | 4.1.2 | A | high | serious |
75
- | `spinbutton-name-present` | Spinbuttons have an accessible name | 4.1.2 | A | high | serious |
76
- | `summary-name-present` | Summary elements have an accessible name | 4.1.2 | A | high | serious |
77
- | `svg-image-text-alternative-present` | SVG <image> must have a text alternative | 1.1.1 | A | medium | serious |
78
- | `svg-text-alternative-present` | <svg> must provide a text alternative | 1.1.1 | A | high | serious |
79
- | `tab-name-present` | Tabs have an accessible name | 4.1.2 | A | high | serious |
80
- | `table-headers-attr-valid` | Table cell "headers" attribute must reference valid header cells | 1.3.1 | A | high | serious |
81
- | `table-th-has-data-cells` | <th> elements must describe at least one data cell | 1.3.1 | A | high | moderate |
82
- | `target-size-minimum` | Pointer targets must be at least 24x24px large, or leave sufficient distance to other targets | 2.5.8 | AA | medium | serious |
83
- | `td-has-header` | Data cells in large tables must have an associated header | 1.3.1 | A | high | serious |
84
- | `textbox-name-present` | Textboxes have an accessible name | 4.1.2 | A | high | serious |
85
- | `tooltip-name-present` | Tooltips have an accessible name | 4.1.2 | A | high | serious |
86
- | `treeitem-name-present` | Tree items have an accessible name | 4.1.2 | A | high | serious |
87
- | `valid-lang` | Element lang attribute must be syntactically valid | 3.1.2 | AA | high | moderate |
88
- | `video-poster-text-alternative-present` | <video> poster must have a text alternative | 1.1.1 | A | medium | serious |
89
-
90
- ## Manual rules (49) — advisory, capped at `cantTell`
15
+ | [`area-alt-present`](#area-alt-present) | <area> must have an alt attribute | 1.1.1 | A | high | serious |
16
+ | [`aria-allowed-attr`](#aria-allowed-attr) | aria-* attributes must be permitted for the element’s role | 4.1.2 | A | medium | moderate |
17
+ | [`aria-allowed-role`](#aria-allowed-role) | Explicit role must be permitted for its host element | — | — | high | moderate |
18
+ | [`aria-braille-equivalent`](#aria-braille-equivalent) | aria-braillelabel/aria-brailleroledescription must have a non-braille equivalent | 4.1.2 | A | high | moderate |
19
+ | [`aria-conditional-attr`](#aria-conditional-attr) | aria-errormessage requires aria-invalid to be set to a non-false value | 4.1.2 | A | high | moderate |
20
+ | [`aria-deprecated-role`](#aria-deprecated-role) | role attribute should not use a deprecated or author-discouraged ARIA role | 4.1.2 | A | high | moderate |
21
+ | [`aria-hidden-body`](#aria-hidden-body) | The document <body> must not be aria-hidden | 1.3.1, 4.1.2 | A | high | critical |
22
+ | [`aria-hidden-focus`](#aria-hidden-focus) | ARIA hidden elements must not be focusable | 2.4.7, 4.1.2 | AA | high | serious |
23
+ | [`aria-prohibited-attr`](#aria-prohibited-attr) | ARIA naming attributes must not be used on roles that prohibit them | 4.1.2 | A | high | moderate |
24
+ | [`aria-prohibited-children`](#aria-prohibited-children) | Container roles must not own a child with a disallowed role | 1.3.1 | A | medium | moderate |
25
+ | [`aria-required-attr`](#aria-required-attr) | Roles with a required ARIA state/property must carry it | 4.1.2 | A | high | serious |
26
+ | [`aria-required-children`](#aria-required-children) | Container roles must own at least one required child role | 1.3.1 | A | medium | moderate |
27
+ | [`aria-required-parent`](#aria-required-parent) | Roles requiring a specific context role must be in that context | 1.3.1 | A | medium | moderate |
28
+ | [`aria-role-name-present`](#aria-role-name-present) | ARIA roles that require an accessible name have one | 4.1.2 | A | high | serious |
29
+ | [`aria-roles-valid`](#aria-roles-valid) | role attribute must be a valid, non-abstract ARIA role | 4.1.2 | A | high | serious |
30
+ | [`aria-valid-attr`](#aria-valid-attr) | aria-* attributes must be real, defined ARIA attributes | 4.1.2 | A | high | serious |
31
+ | [`aria-valid-attr-value`](#aria-valid-attr-value) | aria-* attribute values must match their declared type | 4.1.2 | A | high | serious |
32
+ | [`autocomplete-valid`](#autocomplete-valid) | autocomplete attribute must be a valid autofill value | 1.3.5 | AA | high | moderate |
33
+ | [`avoid-inline-spacing`](#avoid-inline-spacing) | Inline style must not force text spacing below the WCAG metric | 1.4.12 | AA | high | moderate |
34
+ | [`binary-control-name-present`](#binary-control-name-present) | Binary controls have an accessible name | 4.1.2 | A | high | serious |
35
+ | [`button-name-present`](#button-name-present) | Buttons have an accessible name | 4.1.2 | A | high | serious |
36
+ | [`canvas-text-alternative-present`](#canvas-text-alternative-present) | <canvas> must provide a text alternative | 1.1.1 | A | high | serious |
37
+ | [`combobox-name-present`](#combobox-name-present) | Comboboxes have an accessible name | 4.1.2 | A | high | serious |
38
+ | [`contrast-computable`](#contrast-computable) | Color contrast is computable for rendered text | 1.4.3, 1.4.6 | AAA | high | serious |
39
+ | [`contrast-enhanced`](#contrast-enhanced) | Text meets enhanced color contrast (AAA) | 1.4.6 | AAA | high | serious |
40
+ | [`contrast-minimum`](#contrast-minimum) | Text meets minimum color contrast (AA) | 1.4.3 | AA | high | serious |
41
+ | [`css-orientation-lock`](#css-orientation-lock) | CSS must not lock the page to a single orientation | 1.3.4 | AA | high | serious |
42
+ | [`definition-list-children-valid`](#definition-list-children-valid) | Description lists must be structured correctly | 1.3.1 | A | high | serious |
43
+ | [`deprecated-elements-not-used`](#deprecated-elements-not-used) | Obsolete non-stoppable elements (<blink>, <marquee>) must not be used | 2.2.2 | A | high | serious |
44
+ | [`dialog-name-present`](#dialog-name-present) | Dialogs have an accessible name | 4.1.2 | A | high | serious |
45
+ | [`dlitem-parent-valid`](#dlitem-parent-valid) | Description-list items must be inside a description list | 1.3.1 | A | high | serious |
46
+ | [`duplicate-id`](#duplicate-id) | IDs must be unique | 4.1.1 | A | high | moderate |
47
+ | [`duplicate-id-aria`](#duplicate-id-aria) | IDs referenced by ARIA must be unique | 4.1.2 | A | high | serious |
48
+ | [`embed-text-alternative-present`](#embed-text-alternative-present) | <embed> must provide a text alternative | 1.1.1 | A | high | serious |
49
+ | [`form-control-programmatic-label-present`](#form-control-programmatic-label-present) | Form controls must have a programmatic label | 1.3.1, 3.3.2, 4.1.2 | A | medium | serious |
50
+ | [`form-control-single-label`](#form-control-single-label) | Form controls must not have multiple labels | 3.3.2 | A | high | moderate |
51
+ | [`html-lang-attr-present`](#html-lang-attr-present) | Page language is declared | 3.1.1 | A | high | serious |
52
+ | [`html-xml-lang-mismatch`](#html-xml-lang-mismatch) | lang and xml:lang must not disagree | 3.1.1 | A | high | serious |
53
+ | [`identical-iframes-same-purpose`](#identical-iframes-same-purpose) | Frames with the same name embed the same resource | 4.1.2 | A | medium | moderate |
54
+ | [`iframe-focusable-content`](#iframe-focusable-content) | Frames with tabindex="-1" must not contain focusable content | 2.1.1 | A | high | moderate |
55
+ | [`iframe-name-present`](#iframe-name-present) | Frames have an accessible name | 4.1.2 | A | high | serious |
56
+ | [`iframe-title-unique`](#iframe-title-unique) | Frame titles must be unique | 4.1.2 | A | high | moderate |
57
+ | [`img-alt-present`](#img-alt-present) | <img> must have an alt attribute | 1.1.1 | A | high | serious |
58
+ | [`input-image-alt-present`](#input-image-alt-present) | <input type="image"> must have an alt attribute | 1.1.1 | A | high | serious |
59
+ | [`label-in-name`](#label-in-name) | Label in Name: accessible name contains visible text | 2.5.3 | A | high | serious |
60
+ | [`link-in-text-block`](#link-in-text-block) | Links in text blocks must be distinguishable from surrounding text without relying on color alone | 1.4.1 | A | high | serious |
61
+ | [`link-name-present`](#link-name-present) | Links have an accessible name | 4.1.2 | A | high | serious |
62
+ | [`list-children-valid`](#list-children-valid) | Lists must only directly contain list items | 1.3.1 | A | high | serious |
63
+ | [`listbox-name-present`](#listbox-name-present) | Listboxes have an accessible name | 4.1.2 | A | high | serious |
64
+ | [`listitem-parent-valid`](#listitem-parent-valid) | List items must be inside a list container | 1.3.1 | A | high | serious |
65
+ | [`menuitem-name-present`](#menuitem-name-present) | Menu items have an accessible name | 4.1.2 | A | high | serious |
66
+ | [`meta-refresh-no-exceptions`](#meta-refresh-no-exceptions) | Page must not use a meta refresh at all (AAA) | 2.2.4, 3.2.5 | AAA | high | moderate |
67
+ | [`meta-refresh-timing-absent`](#meta-refresh-timing-absent) | Page must not use a timed meta refresh | 2.2.1 | A | high | serious |
68
+ | [`meta-viewport-zoom-enabled`](#meta-viewport-zoom-enabled) | Viewport meta tag must not disable zoom | 1.4.4 | AA | high | serious |
69
+ | [`meter-name-present`](#meter-name-present) | Meters have an accessible name | 1.1.1 | A | high | serious |
70
+ | [`nested-interactive-controls-absent`](#nested-interactive-controls-absent) | Interactive controls must not be nested | 4.1.2 | A | high | serious |
71
+ | [`object-text-alternative-present`](#object-text-alternative-present) | <object> must provide a text alternative | 1.1.1 | A | high | serious |
72
+ | [`option-name-present`](#option-name-present) | Options have an accessible name | 4.1.2 | A | high | serious |
73
+ | [`page-title-present`](#page-title-present) | Page has a non-empty title | 2.4.2 | A | high | serious |
74
+ | [`presentational-children-focusable-absent`](#presentational-children-focusable-absent) | Roles with presentational children must not contain focusable content | 4.1.2 | A | high | serious |
75
+ | [`progressbar-name-present`](#progressbar-name-present) | Progress bars have an accessible name | 1.1.1 | A | high | serious |
76
+ | [`role-img-text-alternative-present`](#role-img-text-alternative-present) | [role="img"/"graphics-symbol"/"graphics-document"] must have an accessible text alternative | 1.1.1 | A | high | serious |
77
+ | [`searchbox-name-present`](#searchbox-name-present) | Searchboxes have an accessible name | 4.1.2 | A | high | serious |
78
+ | [`server-side-image-map-absent`](#server-side-image-map-absent) | Images must not use a server-side image map | 2.1.1 | A | high | serious |
79
+ | [`slider-name-present`](#slider-name-present) | Sliders have an accessible name | 4.1.2 | A | high | serious |
80
+ | [`spinbutton-name-present`](#spinbutton-name-present) | Spinbuttons have an accessible name | 4.1.2 | A | high | serious |
81
+ | [`summary-name-present`](#summary-name-present) | Summary elements have an accessible name | 4.1.2 | A | high | serious |
82
+ | [`svg-image-text-alternative-present`](#svg-image-text-alternative-present) | SVG <image> must have a text alternative | 1.1.1 | A | medium | serious |
83
+ | [`svg-text-alternative-present`](#svg-text-alternative-present) | <svg> must provide a text alternative | 1.1.1 | A | high | serious |
84
+ | [`tab-name-present`](#tab-name-present) | Tabs have an accessible name | 4.1.2 | A | high | serious |
85
+ | [`table-headers-attr-valid`](#table-headers-attr-valid) | Table cell "headers" attribute must reference valid header cells | 1.3.1 | A | high | serious |
86
+ | [`table-th-has-data-cells`](#table-th-has-data-cells) | <th> elements must describe at least one data cell | 1.3.1 | A | high | moderate |
87
+ | [`target-size-minimum`](#target-size-minimum) | Pointer targets must be at least 24x24px large, or leave sufficient distance to other targets | 2.5.8 | AA | medium | serious |
88
+ | [`td-has-header`](#td-has-header) | Data cells in large tables must have an associated header | 1.3.1 | A | high | serious |
89
+ | [`textbox-name-present`](#textbox-name-present) | Textboxes have an accessible name | 4.1.2 | A | high | serious |
90
+ | [`tooltip-name-present`](#tooltip-name-present) | Tooltips have an accessible name | 4.1.2 | A | high | serious |
91
+ | [`treeitem-name-present`](#treeitem-name-present) | Tree items have an accessible name | 4.1.2 | A | high | serious |
92
+ | [`valid-lang`](#valid-lang) | Element lang attribute must be syntactically valid | 3.1.2 | AA | high | moderate |
93
+ | [`video-poster-text-alternative-present`](#video-poster-text-alternative-present) | <video> poster must have a text alternative | 1.1.1 | A | medium | serious |
94
+
95
+ ## Manual rules (54), advisory, capped at `cantTell`
91
96
 
92
97
  | Rule ID | Title | WCAG SC | Level | Confidence | Default severity |
93
98
  |---|---|---|---|---|---|
94
- | `accesskeys` | accesskey values must be unique | — | — | medium | minor |
95
- | `area-alt-decorative` | <area> with alt="" must be decorative (manual review) | 1.1.1 | A | medium | minor |
96
- | `area-alt-quality` | <area> alt text must be appropriate (manual review) | 1.1.1 | A | medium | minor |
97
- | `aria-checked-state-mismatch` | Native checkbox/radio aria-checked should match its actual state | 4.1.2 | A | medium | moderate |
98
- | `aria-text` | role="text" elements should have no focusable descendants | — | — | medium | minor |
99
- | `bypass-blocks-present` | Page must provide a way to bypass repeated blocks | 2.4.1 | A | medium | moderate |
100
- | `canvas-text-alternative-quality` | <canvas> text alternative must be appropriate (manual review) | 1.1.1 | A | medium | minor |
101
- | `css-hidden-focus` | Focusable elements must not be visually hidden | 2.4.7 | AA | low | serious |
102
- | `embed-text-alternative-quality` | <embed> text alternative must be appropriate (manual review) | 1.1.1 | A | medium | minor |
103
- | `empty-heading` | Headings must not be empty | — | — | medium | minor |
104
- | `empty-table-header` | Table header cells must not be empty | — | — | medium | minor |
105
- | `focus-order-semantics` | Elements added to the tab order should have interactive semantics | — | — | medium | minor |
106
- | `form-control-programmatic-label-quality` | Form controls should not rely on placeholder or title as the primary label | 4.1.2 | A | medium | moderate |
107
- | `heading-order` | Heading levels must not skip a level | — | — | medium | minor |
108
- | `identical-links-same-purpose` | Links with the same accessible name should lead to the same destination | 2.4.9 | AAA | low | minor |
109
- | `image-redundant-alt` | Image alt text must not duplicate adjacent visible text | — | — | medium | minor |
110
- | `img-alt-decorative` | <img> with alt="" must be decorative (manual review) | 1.1.1 | A | medium | minor |
111
- | `img-alt-quality` | <img> alt text must be appropriate (manual review) | 1.1.1 | A | medium | minor |
112
- | `input-image-alt-decorative` | <input type="image"> with alt="" must be appropriate (manual review) | 1.1.1 | A | medium | minor |
113
- | `input-image-alt-quality` | <input type="image"> alt text must be appropriate (manual review) | 1.1.1 | A | medium | minor |
114
- | `label-title-only` | Form controls should not use title as their only label | — | — | medium | minor |
115
- | `landmark-banner-is-top-level` | Banner landmark must be top-level | — | — | medium | minor |
116
- | `landmark-contentinfo-is-top-level` | Contentinfo landmark must be top-level | — | — | medium | minor |
117
- | `landmark-main-is-top-level` | Main landmark must be top-level | — | — | medium | minor |
118
- | `landmark-no-duplicate-banner` | Page must not have more than one banner landmark | — | — | medium | minor |
119
- | `landmark-no-duplicate-contentinfo` | Page must not have more than one contentinfo landmark | — | — | medium | minor |
120
- | `landmark-no-duplicate-main` | Page must not have more than one main landmark | — | — | medium | minor |
121
- | `landmark-one-main` | Page should have a main landmark | — | — | medium | minor |
122
- | `landmark-unique` | Landmarks with the same role must have unique names | — | — | medium | minor |
123
- | `link-name-quality` | Link text should be descriptive, not generic | 2.4.4 | A | medium | minor |
124
- | `manual-review` | Manual review: keyboard navigation and focus order | 2.1.1, 2.4.3, 2.4.7 | AA | medium | moderate |
125
- | `media-alternative-transcript-evidence` | Time-based media: transcript or text alternative evidence | 1.2.1 | A | low | moderate |
126
- | `meta-viewport-large` | Viewport meta tag should allow zooming up to 500% | — | — | medium | minor |
127
- | `mouse-only-event-handlers` | Pointer-only inline event handlers should have a keyboard-reachable equivalent | 2.1.1 | A | low | moderate |
128
- | `no-autoplay-audio` | Autoplaying audio should provide a pause/stop or volume-control mechanism | 1.4.2 | A | low | moderate |
129
- | `object-text-alternative-quality` | <object> text alternative must be appropriate (manual review) | 1.1.1 | A | medium | minor |
130
- | `p-as-heading` | A <p> styled to look like a heading should probably be a real heading | 1.3.1 | A | low | minor |
131
- | `page-has-heading-one` | Page should have a level-one heading | — | — | medium | minor |
132
- | `page-title-patterns` | Page title patterns that may be insufficiently descriptive | 2.4.2 | A | medium | minor |
133
- | `presentation-role-conflict` | Presentational role must not conflict with a global ARIA attribute or focusability | — | — | medium | minor |
134
- | `region` | Page content should be inside a landmark region | — | — | medium | minor |
135
- | `scope-attr-valid` | scope attribute must have a valid value | — | — | medium | minor |
136
- | `scrollable-region-focusable` | Scrollable regions with no focusable content should be keyboard-focusable | 2.1.1, 2.1.3 | AAA | low | moderate |
137
- | `skip-link` | Skip link must have a resolvable, usable target | — | — | medium | minor |
138
- | `svg-text-alternative-quality` | <svg> text alternative must be appropriate (manual review) | 1.1.1 | A | medium | minor |
139
- | `tabindex` | tabindex should not be greater than 0 | — | — | medium | minor |
140
- | `table-duplicate-name` | Table caption must not duplicate its summary attribute | — | — | medium | minor |
141
- | `table-fake-caption` | A table's first row should not stand in for a real <caption> | 1.3.1 | A | low | minor |
142
- | `video-caption` | Prerecorded video should provide a captions track | 1.2.2 | A | low | moderate |
143
-
144
- ## Composite (WCAG-SC rollup) rules (31)
145
-
146
- Composite rules aren't individually authored — they're generated rollups over the atomic rules above, one per WCAG Success Criterion with automatable coverage. See [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md) for rollup semantics.
147
-
148
- | Composite ID | Title | WCAG SC | Level | # atomic rules rolled up |
149
- |---|---|---|---|---|
150
- | `wcag-1.1.1-non-text-content` | Non-text content: text alternatives | 1.1.1 | A | 22 |
151
- | `wcag-1.2.1-audio-only-video-only-prerecorded` | Audio-only and video-only (prerecorded): transcript | 1.2.1 | A | 1 |
152
- | `wcag-1.2.2-captions-prerecorded` | Captions (Prerecorded) | 1.2.2 | A | 1 |
153
- | `wcag-1.3.1-info-and-relationships` | Info and Relationships | 1.3.1 | A | 11 |
154
- | `wcag-1.3.4-orientation` | Orientation | 1.3.4 | AA | 1 |
155
- | `wcag-1.3.5-identify-input-purpose` | Identify Input Purpose | 1.3.5 | AA | 1 |
156
- | `wcag-1.4.1-use-of-color` | Use of Color | 1.4.1 | A | 1 |
157
- | `wcag-1.4.12-text-spacing` | Text Spacing | 1.4.12 | AA | 1 |
158
- | `wcag-1.4.2-audio-control` | Audio Control | 1.4.2 | A | 1 |
159
- | `wcag-1.4.3-contrast-minimum` | Contrast: minimum | 1.4.3 | AA | 2 |
160
- | `wcag-1.4.4-resize-text` | Resize Text | 1.4.4 | AA | 1 |
161
- | `wcag-1.4.6-contrast-enhanced` | Contrast: enhanced | 1.4.6 | AAA | 2 |
162
- | `wcag-2.1.1-keyboard` | Keyboard | 2.1.1 | A | 5 |
163
- | `wcag-2.1.3-keyboard-no-exception` | Keyboard (No Exception) | 2.1.3 | AAA | 1 |
164
- | `wcag-2.2.1-timing-adjustable` | Timing Adjustable | 2.2.1 | A | 1 |
165
- | `wcag-2.2.2-pause-stop-hide` | Pause, Stop, Hide | 2.2.2 | A | 1 |
166
- | `wcag-2.2.4-interruptions` | Interruptions | 2.2.4 | AAA | 1 |
167
- | `wcag-2.4.1-bypass-blocks` | Bypass Blocks | 2.4.1 | A | 1 |
168
- | `wcag-2.4.2-page-titled` | Page titled | 2.4.2 | A | 2 |
169
- | `wcag-2.4.3-focus-order` | Focus order | 2.4.3 | A | 1 |
170
- | `wcag-2.4.4-link-purpose-in-context` | Link Purpose (In Context) | 2.4.4 | A | 1 |
171
- | `wcag-2.4.7-focus-visible` | Focus visible | 2.4.7 | AA | 3 |
172
- | `wcag-2.4.9-link-purpose-link-only` | Link Purpose (Link Only) | 2.4.9 | AAA | 1 |
173
- | `wcag-2.5.3-label-in-name` | Label in name | 2.5.3 | A | 1 |
174
- | `wcag-2.5.8-target-size-minimum` | Target size: minimum | 2.5.8 | AA | 1 |
175
- | `wcag-3.1.1-language-of-page` | Language of page | 3.1.1 | A | 2 |
176
- | `wcag-3.1.2-language-of-parts` | Language of Parts | 3.1.2 | AA | 1 |
177
- | `wcag-3.2.5-change-on-request` | Change on Request | 3.2.5 | AAA | 1 |
178
- | `wcag-3.3.2-labels-or-instructions` | Labels or Instructions | 3.3.2 | A | 2 |
179
- | `wcag-4.1.2-aria-validity` | Name, role, value: ARIA validity | 4.1.2 | A | 16 |
180
- | `wcag-4.1.2-name` | Name, role, value: accessible name | 4.1.2 | A | 23 |
99
+ | [`accesskeys`](#accesskeys) | accesskey values must be unique | — | — | medium | minor |
100
+ | [`area-alt-decorative`](#area-alt-decorative) | <area> with alt="" must be decorative (manual review) | 1.1.1 | A | medium | minor |
101
+ | [`area-alt-quality`](#area-alt-quality) | <area> alt text must be appropriate (manual review) | 1.1.1 | A | medium | minor |
102
+ | [`aria-checked-state-mismatch`](#aria-checked-state-mismatch) | Native checkbox/radio aria-checked should match its actual state | 4.1.2 | A | medium | moderate |
103
+ | [`aria-text`](#aria-text) | role="text" elements should have no focusable descendants | — | — | medium | minor |
104
+ | [`bypass-blocks-present`](#bypass-blocks-present) | Page must provide a way to bypass repeated blocks | 2.4.1 | A | medium | moderate |
105
+ | [`canvas-text-alternative-quality`](#canvas-text-alternative-quality) | <canvas> text alternative must be appropriate (manual review) | 1.1.1 | A | medium | minor |
106
+ | [`css-focus-indicator-suppressed`](#css-focus-indicator-suppressed) | Focus indicator must not be removed without a replacement | 2.4.7 | AA | medium | serious |
107
+ | [`css-hidden-focus`](#css-hidden-focus) | Focusable elements must not be visually hidden | 2.4.7 | AA | low | serious |
108
+ | [`embed-text-alternative-quality`](#embed-text-alternative-quality) | <embed> text alternative must be appropriate (manual review) | 1.1.1 | A | medium | minor |
109
+ | [`empty-heading`](#empty-heading) | Headings must not be empty | — | — | medium | minor |
110
+ | [`empty-table-header`](#empty-table-header) | Table header cells must not be empty | — | — | medium | minor |
111
+ | [`focus-order-semantics`](#focus-order-semantics) | Elements added to the tab order should have interactive semantics | — | — | medium | minor |
112
+ | [`form-control-label-quality`](#form-control-label-quality) | Form field labels should be descriptive and distinguishable | 2.4.6 | AA | medium | minor |
113
+ | [`form-control-programmatic-label-quality`](#form-control-programmatic-label-quality) | Form controls should not rely on placeholder or title as the primary label | 4.1.2 | A | medium | moderate |
114
+ | [`heading-order`](#heading-order) | Heading levels must not skip a level | — | — | medium | minor |
115
+ | [`heading-quality`](#heading-quality) | Heading text should be descriptive, not a placeholder | 2.4.6 | AA | medium | minor |
116
+ | [`identical-links-same-purpose`](#identical-links-same-purpose) | Links with the same accessible name should lead to the same destination | 2.4.9 | AAA | low | minor |
117
+ | [`image-redundant-alt`](#image-redundant-alt) | Image alt text must not duplicate adjacent visible text | — | — | medium | minor |
118
+ | [`img-alt-decorative`](#img-alt-decorative) | Excluded <img>/<canvas>/<svg> must be decorative (manual review) | 1.1.1 | A | medium | minor |
119
+ | [`img-alt-quality`](#img-alt-quality) | <img> alt text must be appropriate (manual review) | 1.1.1 | A | medium | minor |
120
+ | [`input-image-alt-decorative`](#input-image-alt-decorative) | <input type="image"> with alt="" must be appropriate (manual review) | 1.1.1 | A | medium | minor |
121
+ | [`input-image-alt-quality`](#input-image-alt-quality) | <input type="image"> alt text must be appropriate (manual review) | 1.1.1 | A | medium | minor |
122
+ | [`label-title-only`](#label-title-only) | Form controls should not use title as their only label | — | — | medium | minor |
123
+ | [`landmark-banner-is-top-level`](#landmark-banner-is-top-level) | Banner landmark must be top-level | — | — | medium | minor |
124
+ | [`landmark-complementary-is-top-level`](#landmark-complementary-is-top-level) | Complementary landmark must be top-level | — | — | medium | minor |
125
+ | [`landmark-contentinfo-is-top-level`](#landmark-contentinfo-is-top-level) | Contentinfo landmark must be top-level | — | — | medium | minor |
126
+ | [`landmark-main-is-top-level`](#landmark-main-is-top-level) | Main landmark must be top-level | — | — | medium | minor |
127
+ | [`landmark-no-duplicate-banner`](#landmark-no-duplicate-banner) | Page must not have more than one banner landmark | — | — | medium | minor |
128
+ | [`landmark-no-duplicate-contentinfo`](#landmark-no-duplicate-contentinfo) | Page must not have more than one contentinfo landmark | — | — | medium | minor |
129
+ | [`landmark-no-duplicate-main`](#landmark-no-duplicate-main) | Page must not have more than one main landmark | — | — | medium | minor |
130
+ | [`landmark-one-main`](#landmark-one-main) | Page should have a main landmark | — | — | medium | minor |
131
+ | [`landmark-unique`](#landmark-unique) | Landmarks with the same role must have unique names | — | — | medium | minor |
132
+ | [`link-name-quality`](#link-name-quality) | Link text should be descriptive, not generic | 2.4.4 | A | medium | minor |
133
+ | [`manual-review`](#manual-review) | Manual review: keyboard navigation and focus order | 2.1.1, 2.4.3, 2.4.7 | AA | medium | moderate |
134
+ | [`media-alternative-transcript-evidence`](#media-alternative-transcript-evidence) | Time-based media: transcript or text alternative evidence | 1.2.1 | A | low | moderate |
135
+ | [`meta-viewport-large`](#meta-viewport-large) | Viewport meta tag should allow zooming up to 500% | — | — | medium | minor |
136
+ | [`mouse-only-event-handlers`](#mouse-only-event-handlers) | Pointer-only inline event handlers should have a keyboard-reachable equivalent | 2.1.1 | A | low | moderate |
137
+ | [`no-autoplay-audio`](#no-autoplay-audio) | Autoplaying audio should provide a pause/stop or volume-control mechanism | 1.4.2 | A | low | moderate |
138
+ | [`object-text-alternative-quality`](#object-text-alternative-quality) | <object> text alternative must be appropriate (manual review) | 1.1.1 | A | medium | minor |
139
+ | [`p-as-heading`](#p-as-heading) | A <p> styled to look like a heading should probably be a real heading | 1.3.1 | A | low | minor |
140
+ | [`page-has-heading-one`](#page-has-heading-one) | Page should have a level-one heading | — | — | medium | minor |
141
+ | [`page-title-patterns`](#page-title-patterns) | Page title patterns that may be insufficiently descriptive | 2.4.2 | A | medium | minor |
142
+ | [`password-paste-enabled`](#password-paste-enabled) | Authentication fields must not block pasting | 3.3.8 | AA | medium | serious |
143
+ | [`presentation-role-conflict`](#presentation-role-conflict) | Presentational role must not conflict with a global ARIA attribute or focusability | — | — | medium | minor |
144
+ | [`region`](#region) | Page content should be inside a landmark region | — | — | medium | minor |
145
+ | [`scope-attr-valid`](#scope-attr-valid) | scope attribute must have a valid value | — | — | medium | minor |
146
+ | [`scrollable-region-focusable`](#scrollable-region-focusable) | Scrollable regions with no focusable content should be keyboard-focusable | 2.1.1, 2.1.3 | AAA | low | moderate |
147
+ | [`skip-link`](#skip-link) | Skip link must have a resolvable, usable target | — | — | medium | minor |
148
+ | [`svg-text-alternative-quality`](#svg-text-alternative-quality) | <svg> text alternative must be appropriate (manual review) | 1.1.1 | A | medium | minor |
149
+ | [`tabindex`](#tabindex) | tabindex should not be greater than 0 | — | — | medium | minor |
150
+ | [`table-duplicate-name`](#table-duplicate-name) | Table caption must not duplicate its summary attribute | — | — | medium | minor |
151
+ | [`table-fake-caption`](#table-fake-caption) | A table's first row should not stand in for a real <caption> | 1.3.1 | A | low | minor |
152
+ | [`video-caption`](#video-caption) | Prerecorded video should provide a captions track | 1.2.2 | A | low | moderate |
153
+
154
+ ## Composite (WCAG-SC rollup) rules (34)
155
+
156
+ Composite rules aren't individually authored. They're generated rollups over the atomic rules above, one per WCAG Success Criterion with automatable coverage. See [`WCAG_CONFORMANCE.md`](./WCAG_CONFORMANCE.md) for rollup semantics.
157
+
158
+ | Composite ID | Title | Description | WCAG SC | Level | # atomic rules rolled up |
159
+ |---|---|---|---|---|---|
160
+ | `wcag-1.1.1-non-text-content` | Non-text content: text alternatives | Rollup of checks ensuring non-text content has an appropriate text alternative. | 1.1.1 | A | 22 |
161
+ | `wcag-1.2.1-audio-only-video-only-prerecorded` | Audio-only and video-only (prerecorded): transcript | Rollup of checks for transcript availability for prerecorded audio-only/video-only media. | 1.2.1 | A | 1 |
162
+ | `wcag-1.2.2-captions-prerecorded` | Captions (Prerecorded) | Rollup of checks for captions-track evidence on prerecorded video. | 1.2.2 | A | 1 |
163
+ | `wcag-1.3.1-info-and-relationships` | Info and Relationships | Rollup of checks ensuring information, structure, and relationships conveyed through presentation are programmatically determinable. | 1.3.1 | A | 14 |
164
+ | `wcag-1.3.4-orientation` | Orientation | Rollup of checks ensuring content does not restrict its view to a single display orientation. | 1.3.4 | AA | 1 |
165
+ | `wcag-1.3.5-identify-input-purpose` | Identify Input Purpose | Rollup of checks ensuring the autocomplete attribute correctly identifies input purpose. | 1.3.5 | AA | 1 |
166
+ | `wcag-1.4.1-use-of-color` | Use of Color | Rollup of checks ensuring color is not used as the only visual means of conveying information. | 1.4.1 | A | 1 |
167
+ | `wcag-1.4.12-text-spacing` | Text Spacing | Rollup of checks ensuring inline styles do not block user text-spacing overrides. | 1.4.12 | AA | 1 |
168
+ | `wcag-1.4.2-audio-control` | Audio Control | Rollup of checks for a pause/stop or volume-control mechanism on autoplaying audio. | 1.4.2 | A | 1 |
169
+ | `wcag-1.4.3-contrast-minimum` | Contrast: minimum | Rollup of checks for minimum text contrast. | 1.4.3 | AA | 2 |
170
+ | `wcag-1.4.4-resize-text` | Resize Text | Rollup of checks ensuring the viewport meta tag does not prevent users from zooming text up to 200%. | 1.4.4 | AA | 1 |
171
+ | `wcag-1.4.6-contrast-enhanced` | Contrast: enhanced | Rollup of checks for enhanced text contrast. | 1.4.6 | AAA | 2 |
172
+ | `wcag-2.1.1-keyboard` | Keyboard | Rollup of checks ensuring functionality is operable through a keyboard interface. | 2.1.1 | A | 5 |
173
+ | `wcag-2.1.3-keyboard-no-exception` | Keyboard (No Exception) | Rollup of checks ensuring functionality is operable through a keyboard interface with no exceptions (AAA). | 2.1.3 | AAA | 1 |
174
+ | `wcag-2.2.1-timing-adjustable` | Timing Adjustable | Rollup of checks ensuring the page does not impose a timed refresh the user cannot control. | 2.2.1 | A | 1 |
175
+ | `wcag-2.2.2-pause-stop-hide` | Pause, Stop, Hide | Rollup of checks ensuring moving, blinking, or auto-scrolling content can be paused, stopped, or hidden. | 2.2.2 | A | 1 |
176
+ | `wcag-2.2.4-interruptions` | Interruptions | Rollup of checks ensuring automatic context changes only happen at the user's request (AAA). | 2.2.4 | AAA | 1 |
177
+ | `wcag-2.4.1-bypass-blocks` | Bypass Blocks | Rollup of checks ensuring the page provides a way to bypass repeated blocks of content. | 2.4.1 | A | 1 |
178
+ | `wcag-2.4.2-page-titled` | Page titled | Rollup of checks ensuring documents have a meaningful page title. | 2.4.2 | A | 2 |
179
+ | `wcag-2.4.3-focus-order` | Focus order | Rollup of checks ensuring focus moves through content in a meaningful order. | 2.4.3 | A | 1 |
180
+ | `wcag-2.4.4-link-purpose-in-context` | Link Purpose (In Context) | Rollup of checks flagging links whose text alone is a known non-descriptive/generic phrase. | 2.4.4 | A | 1 |
181
+ | `wcag-2.4.6-headings-and-labels` | Headings and Labels | Rollup of checks flagging headings whose text is a placeholder rather than a description of the content that follows. | 2.4.6 | AA | 2 |
182
+ | `wcag-2.4.7-focus-visible` | Focus visible | Rollup of checks ensuring keyboard focus is not hidden and remains perceivable. | 2.4.7 | AA | 4 |
183
+ | `wcag-2.4.9-link-purpose-link-only` | Link Purpose (Link Only) | Rollup of checks ensuring links with the same accessible name serve the same purpose (AAA). | 2.4.9 | AAA | 1 |
184
+ | `wcag-2.5.3-label-in-name` | Label in name | Rollup of checks ensuring that when a control has a visible text label, the accessible name contains that visible label text. | 2.5.3 | A | 1 |
185
+ | `wcag-2.5.8-target-size-minimum` | Target size: minimum | Rollup of checks ensuring pointer targets meet minimum size requirements. | 2.5.8 | AA | 1 |
186
+ | `wcag-3.1.1-language-of-page` | Language of page | Rollup of checks ensuring the page language is specified. | 3.1.1 | A | 2 |
187
+ | `wcag-3.1.2-language-of-parts` | Language of Parts | Rollup of checks ensuring elements whose language differs from the page default declare it correctly. | 3.1.2 | AA | 1 |
188
+ | `wcag-3.2.5-change-on-request` | Change on Request | Rollup of checks ensuring context changes only happen at the user's request (AAA). | 3.2.5 | AAA | 1 |
189
+ | `wcag-3.3.2-labels-or-instructions` | Labels or Instructions | Rollup of checks ensuring form controls have unambiguous labeling. | 3.3.2 | A | 2 |
190
+ | `wcag-3.3.8-accessible-authentication-minimum` | Accessible Authentication (Minimum) | Rollup of checks ensuring an authentication step leaves the mechanisms that help a user through it in place. | 3.3.8 | AA | 1 |
191
+ | `wcag-4.1.1-parsing` | Parsing | Rollup of checks ensuring id values are unique. WCAG 2.0/2.1 only: SC 4.1.1 was removed in WCAG 2.2, so this composite carries the wcag22-removed tag. | 4.1.1 | A | 1 |
192
+ | `wcag-4.1.2-aria-validity` | Name, role, value: ARIA validity | Rollup of checks that ARIA role and attribute usage conforms to the WAI-ARIA specification (valid roles, valid attributes, valid values, required attributes, unique ARIA-referenced ids). | 4.1.2 | A | 13 |
193
+ | `wcag-4.1.2-name` | Name, role, value: accessible name | Rollup of checks that common interactive elements expose a non-empty accessible name. | 4.1.2 | A | 24 |
194
+
195
+ ## Rule reference
196
+
197
+ Every atomic rule, alphabetically. "Applies to" is the rule's precondition (when it returns `notApplicable`), and "Expectation" is the condition it decides once it does apply.
198
+
199
+ ### `accesskeys`
200
+
201
+ **accesskey values must be unique**
202
+
203
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
204
+
205
+ Checks that no two elements on the page share the same accesskey attribute value.
206
+
207
+ **Applies to.** Applies whenever two or more elements share the same non-empty accesskey attribute value (case-insensitive).
208
+
209
+ **Expectation.** Every accesskey value on the page is unique. Duplicate accesskeys make keyboard-shortcut activation ambiguous: only one of the elements sharing the key can actually be reached by it, and which one is browser/platform-dependent.
210
+
211
+ ### `area-alt-decorative`
212
+
213
+ **<area> with alt="" must be decorative (manual review)**
214
+
215
+ manual · WCAG 1.1.1 (A) · confidence medium · default severity minor
216
+
217
+ Flags <area> elements with empty alt for human review that they are decorative/non-informative.
218
+
219
+ **Applies to.** Applies to <area> elements whose alt attribute is present but empty once trimmed, the markup that declares a hotspot decorative. The <area> must belong to a <map> that an <img usemap> actually references, and both that <img> and the <area> itself must be included in the accessibility tree; an <area> in an unused map is out of scope. role="presentation"/"none" takes an element out unless it is focusable.
220
+
221
+ **Expectation.** Human review is required to confirm that the provided text alternative is accurate and appropriate.
222
+
223
+ ### `area-alt-present`
224
+
225
+ **<area> must have an alt attribute**
226
+
227
+ automatic · WCAG 1.1.1 (A) · confidence high · default severity serious
228
+
229
+ Checks that <area> elements provide an alt attribute to support a text alternative mechanism.
230
+
231
+ **Applies to.** Applies to <area> elements that: 1) are in a <map> that is referenced by an <img usemap>, AND 2) the referencing <img> is eligible in the accessibility tree (best-effort), AND 3) the <area> itself is eligible in the accessibility tree (with engine exceptions).
232
+
233
+ **Expectation.** Each applicable <area> element has an alt attribute. The alt attribute may be empty (alt="").
234
+
235
+ ### `area-alt-quality`
236
+
237
+ **<area> alt text must be appropriate (manual review)**
238
+
239
+ manual · WCAG 1.1.1 (A) · confidence medium · default severity minor
240
+
241
+ Flags <area> elements with non-empty alt text for human review of appropriateness.
242
+
243
+ **Applies to.** Applies to <area> elements whose alt attribute is present and non-empty. The <area> must belong to a <map> that an <img usemap> actually references, and both that <img> and the <area> itself must be included in the accessibility tree; an <area> in an unused map is out of scope. role="presentation"/"none" takes an element out unless it is focusable.
244
+
245
+ **Expectation.** Human review is required to confirm that the provided text alternative is accurate and appropriate.
246
+
247
+ ### `aria-allowed-attr`
248
+
249
+ **aria-* attributes must be permitted for the element’s role**
250
+
251
+ automatic · WCAG 4.1.2 (A) · confidence medium · default severity moderate
252
+
253
+ Checks that every recognized aria-* attribute present on an element with an explicit role is either globally supported or supported by that role.
254
+
255
+ **Applies to.** Applies to elements carrying at least one recognized, non-global aria-* attribute, judged against the role they actually have: an explicit valid role, else the implicit role of their tag, else, for the elements HTML-AAM maps to no role at all, nothing.
256
+
257
+ **Expectation.** Every recognized aria-* attribute present is either: (a) globally supported on any element (the "global" ARIA states/properties, e.g. aria-label/aria-hidden/aria-describedby), or (b) explicitly listed as a required or supported state/property for the element's role. An attribute ARIA deprecated (rather than prohibited) on the role is still allowed: it is reported as CANTTELL (see helpers.aria.isDeprecatedAttr) so the author decides, not as a not-allowed FAIL.
258
+
259
+ ### `aria-allowed-role`
260
+
261
+ **Explicit role must be permitted for its host element**
262
+
263
+ automatic · no formal WCAG SC mapping · confidence high · default severity moderate
264
+
265
+ Checks that an explicit role="" attribute is one of the roles the ARIA-in-HTML specification permits for the host element (e.g. role="tab" is not permitted on <nav>).
266
+
267
+ **Applies to.** Applies to elements with an explicit, valid, non-abstract role, where the host element/attribute combination has an asserted permitted-roles constraint in the ARIA-in-HTML table (src/core/aria-helpers.js ALLOWED_ROLES_BY_ELEMENT).
268
+
269
+ **Expectation.** The explicit role is one of the roles the ARIA-in-HTML specification permits for that host element. Reported at CANTTELL rather than FAIL: ARIA-in-HTML's permitted-roles table is an author conformance requirement with no ACT rule and no WCAG mapping in any source. The role the author asked for is still the role assistive technology exposes, so whether the combination harms anyone depends on the widget, not on the table.
270
+
271
+ ### `aria-braille-equivalent`
272
+
273
+ **aria-braillelabel/aria-brailleroledescription must have a non-braille equivalent**
274
+
275
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity moderate
276
+
277
+ Checks that elements using aria-braillelabel also have a regular accessible name, and elements using aria-brailleroledescription also have aria-roledescription.
278
+
279
+ **Applies to.** Elements with a non-empty `aria-braillelabel` and/or non-empty `aria-brailleroledescription` attribute.
280
+
281
+ **Expectation.**
282
+
283
+ Per the ARIA specification, `aria-braillelabel` is a Braille-specific SUPPLEMENT to (not a replacement for) the element's regular accessible name, and `aria-brailleroledescription` is a supplement to `aria-roledescription`. An element must therefore also have:
284
+
285
+ - a non-empty accessible name from a non-braille mechanism, if it declares `aria-braillelabel`;
286
+ - a non-empty `aria-roledescription`, if it declares `aria-brailleroledescription`.
287
+
288
+ Using either braille-specific attribute as the ONLY naming mechanism leaves non-braille assistive technology (most screen readers, voice control, etc.) with no accessible name/role description at all. Reported at CANTTELL rather than FAIL: aria-brailleroledescription without aria-roledescription reaches no user at all, and a missing accessible name is the naming rules' decision for the roles that require one. The braille attribute being unpaired is worth surfacing, but it is not itself a criterion failing.
289
+
290
+ ### `aria-checked-state-mismatch`
291
+
292
+ **Native checkbox/radio aria-checked should match its actual state**
293
+
294
+ manual · WCAG 4.1.2 (A) · confidence medium · default severity moderate
295
+
296
+ Flags a native <input type="checkbox">/<input type="radio"> whose explicit aria-checked value disagrees with its actual checked/indeterminate state, for manual review.
297
+
298
+ **Applies to.** Native `<input type="checkbox">` / `<input type="radio">` elements that carry an explicit `aria-checked` attribute.
299
+
300
+ **Expectation.** `aria-checked` is redundant on a native checkbox/radio (the role's checked state is already exposed natively), but when an author sets it explicitly it should agree with the element's actual state, otherwise assistive technology is told something different from what a sighted user perceives.
301
+
302
+ ### `aria-conditional-attr`
303
+
304
+ **aria-errormessage requires aria-invalid to be set to a non-false value**
305
+
306
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity moderate
307
+
308
+ Checks that elements with aria-errormessage also have aria-invalid set to "true", "grammar", or "spelling"; otherwise the error message is dropped from the accessibility tree.
309
+
310
+ **Applies to.** Elements with a non-empty `aria-errormessage` attribute.
311
+
312
+ **Expectation.** Per the ARIA specification, `aria-errormessage` is only exposed to assistive technology when `aria-invalid` is present with a value other than `"false"` (i.e. `"true"`, `"grammar"`, or `"spelling"`). An element with `aria-errormessage` but `aria-invalid` absent or `"false"` silently drops the error message from the accessibility tree, authors almost always intend it to be exposed. Reported at CANTTELL rather than FAIL: aria-errormessage is only exposed once aria-invalid is set, so the reference is currently inert. Whether that costs the user anything depends on whether the message is conveyed some other way (visible text next to the field, aria-describedby), which static markup does not settle.
313
+
314
+ ### `aria-deprecated-role`
315
+
316
+ **role attribute should not use a deprecated or author-discouraged ARIA role**
317
+
318
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity moderate
319
+
320
+ Checks that an explicit role="" attribute does not use a role deprecated by the WAI-ARIA specification, or one reserved for user-agent-internal use (e.g. role="generic").
321
+
322
+ **Applies to.** Applies to any element whose role attribute's first (used) token is a valid, non-abstract ARIA role that authors should not explicitly declare, either because WAI-ARIA has deprecated it (e.g. "directory", superseded by role="list") or because it is reserved for user-agent- internal use (role="generic", which ARIA 1.2 §5.4 says authors SHOULD NOT use in content).
323
+
324
+ **Expectation.**
325
+
326
+ The role in use is neither deprecated nor reserved. Graded by the strength of the rule ARIA states:
327
+
328
+ - CANTTELL at SHOULD NOT, which leaves the usage conforming, so the author decides whether it matters: a deprecated role ("directory") or one reserved for user agents ("generic").
329
+ - FAIL at MUST NOT. No ARIA 1.2 or 1.3 role carries an author MUST NOT outside the abstract roles, so this outcome is reserved for a later revision promoting a role to that strength.
330
+
331
+ Distinct, atomic decision from aria-roles-valid (existence/ abstractness): a role can be valid and non-abstract while still being discouraged in explicit author use.
332
+
333
+ ### `aria-hidden-body`
334
+
335
+ **The document <body> must not be aria-hidden**
336
+
337
+ automatic · WCAG 1.3.1, 4.1.2 (A) · confidence high · default severity critical
338
+
339
+ Checks that <body> does not have aria-hidden="true", which would remove the entire page from the accessibility tree.
340
+
341
+ **Applies to.** Always applicable to any HTML document with a <body> element, independent of contextSelector/root scoping, this is a whole- document concern, matching page-title-present's pattern of evaluating document.body directly rather than the scoped root.
342
+
343
+ **Expectation.** <body> does not have aria-hidden="true". Hiding the document body removes the entire page's content and structure from the accessibility tree at once, both 1.3.1 (Info and Relationships: the page's structure becomes entirely non-determinable) and 4.1.2 (Name, Role, Value: nothing in the document exposes a role/name/value any longer) apply.
344
+
345
+ ### `aria-hidden-focus`
346
+
347
+ **ARIA hidden elements must not be focusable**
348
+
349
+ automatic · WCAG 2.4.7, 4.1.2 (AA) · confidence high · default severity serious
350
+
351
+ Checks that aria-hidden="true" elements are not focusable and do not contain focusable descendants.
352
+
353
+ **Applies to.** Applies to elements that have aria-hidden="true".
354
+
355
+ **Expectation.**
356
+
357
+ No element with aria-hidden="true" may itself be focusable, and no focusable element may exist within an aria-hidden="true" subtree. Notes:
358
+
359
+ - Focusability is computed via ctx.helpers.getFocusableInfo (native + tabindex + contenteditable).
360
+ - Elements that are not rendered (e.g., display:none, visibility:hidden, [hidden]) are excluded.
361
+ - Elements hidden via CSS in ways that still allow keyboard focus (e.g., opacity:0, off-screen, clip) remain in-scope and will be flagged when focusable.
362
+
363
+ ### `aria-prohibited-attr`
364
+
365
+ **ARIA naming attributes must not be used on roles that prohibit them**
366
+
367
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity moderate
368
+
369
+ Checks that aria-label/aria-labelledby are not present on WAI-ARIA roles whose specification explicitly prohibits ARIA naming (e.g. generic, emphasis, strong, paragraph).
370
+
371
+ **Applies to.** Applies to (a) elements whose explicit, valid role is one of the ARIA 1.2 roles with a documented "Prohibited ARIA States and Properties" list for naming attributes (pure text-semantics / non-naming structural roles: caption, code, deletion, emphasis, generic, insertion, mark, none, paragraph, presentation, strong, subscript, suggestion, superscript, time), and (b) elements with no role at all: a curated set of native HTML tags verified to carry no implicit role (see ROLELESS_NATIVE_TAGS below), or any autonomous custom element (a hyphenated, author-defined tag per the Custom Elements spec; see isRolelessCustomElementTag below). In both cases, only elements that also carry aria-label or aria-labelledby.
372
+
373
+ **Expectation.** Prohibited attributes must not be present on (a); for (b), the naming attribute is at best unreliable (nothing accessible-name-aware to hang it off) and at worst silently ignored by assistive technology. See the roleless-branch implementation note below for the confidence split this produces.
374
+
375
+ ### `aria-prohibited-children`
376
+
377
+ **Container roles must not own a child with a disallowed role**
378
+
379
+ automatic · WCAG 1.3.1 (A) · confidence medium · default severity moderate
380
+
381
+ Checks that every accessible-tree-owned child of a container role (list, listbox, menu, menubar, radiogroup, rowgroup, table, grid, treegrid, tablist, tree, row) has one of that role's allowed owned roles.
382
+
383
+ **Applies to.** Applies to elements with an explicit, valid role that is one of the container roles with a documented "required owned elements" entry (the same REQUIRED_OWNED_ROLES table aria-required-children uses, see src/core/aria-helpers.js).
384
+
385
+ **Expectation.** Every accessible-tree-owned descendant of the container (after pruning role="none"/"presentation" elements and any "group"/ "rowgroup" wrapper, both always transparent for owned-element matching per WAI-ARIA, regardless of whether "group"/"rowgroup" is itself in the container's own required-owned-roles set) has a role from that same required-owned set. Nothing else is a structurally valid direct child of a composite/container role, where "allowed" is the container's required-owned roles plus the small ALLOWED_EXTRA_OWNED_ROLES set of roles it may own without being required to (a separator between menu items, a caption on a grid). A roleless wrapper is descended into to reach the items a component library buries inside it, but once one is found there the rest of that wrapper's subtree is the item's own content and is not judged against the container.
386
+
387
+ ### `aria-required-attr`
388
+
389
+ **Roles with a required ARIA state/property must carry it**
390
+
391
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
392
+
393
+ Checks that elements with an explicit role carry every unambiguous, context-independent required aria-* state/property for that role (e.g. role="checkbox" must have aria-checked).
394
+
395
+ **Applies to.** Applies to elements with an explicit, valid, non-abstract role that is also one of the small set of roles with a documented, context- independent required state/property (checkbox, combobox, heading, menuitemcheckbox, menuitemradio, meter, radio, scrollbar, separator, slider, switch) -- except when that explicit role is identical to the element's own native/implicit role (ACT 4e8ab6: e.g. <input type="checkbox" role="checkbox">, which is exempt because the native control's own state exposure already covers it; no aria-checked is required. helpers.aria.getNativeRoleForElement resolves this).
396
+
397
+ **Expectation.**
398
+
399
+ Every required state/property for that role is present and non-empty. Graded by whether ARIA supplies a stand-in for the missing attribute:
400
+
401
+ - FAIL where it does not, so the state is simply not exposed (aria-checked on checkbox/radio/switch/menuitemcheckbox/menuitemradio, aria-valuenow on slider/scrollbar/meter and on a focusable separator).
402
+ - CANTTELL where ARIA defines an implicit value the role falls back to (aria-expanded on combobox, aria-level on heading), so the role still exposes a value and only the author knows whether it is the right one.
403
+
404
+ ### `aria-required-children`
405
+
406
+ **Container roles must own at least one required child role**
407
+
408
+ automatic · WCAG 1.3.1 (A) · confidence medium · default severity moderate
409
+
410
+ Checks that container roles with a documented "required owned elements" entry (list, listbox, menu, radiogroup, table, grid, tablist, tree, row, ...) contain at least one descendant or aria-owns-referenced element with an acceptable owned role.
411
+
412
+ **Applies to.** Applies to elements with an explicit, valid, non-abstract role that is also one of the container roles with a documented "required owned elements" entry (list, listbox, menu, menubar, radiogroup, rowgroup, table, grid, treegrid, tablist, tree, row).
413
+
414
+ **Expectation.** At least one descendant, or one aria-owns-referenced element, has one of the acceptable owned roles for that container role. Reported at CANTTELL, never FAIL: this rule asks only whether the required content is PRESENT, and a container that owns nothing conveys nothing false -- an empty role="list" is announced as a list with no items, which is what it is. Whether the content a container does own is VALID is aria-prohibited-children's decision, and that rule still fails, so a genuinely misdescribed structure (a role="button" among list items, a tablist of plain buttons) is caught with the same strength as before. The native-HTML equivalents already work this way: nothing in this ruleset fails an empty <ul>, and list-children-valid judges only the children that exist.
415
+
416
+ ### `aria-required-parent`
417
+
418
+ **Roles requiring a specific context role must be in that context**
419
+
420
+ automatic · WCAG 1.3.1 (A) · confidence medium · default severity moderate
421
+
422
+ Checks that roles with a documented "required context role" entry (listitem, option, tab, treeitem, row, cell, ...) have an ancestor or aria-owns owner with an acceptable context role.
423
+
424
+ **Applies to.** Applies to elements with an explicit, valid, non-abstract role that is also one of the roles with a documented, non-empty "required context role" entry (listitem, option, menuitem, menuitemcheckbox, menuitemradio, tab, treeitem, row, cell, gridcell, columnheader, rowheader, rowgroup).
425
+
426
+ **Expectation.** The element has an ancestor (DOM containment) or owner (via that ancestor/owner's aria-owns) whose effective role is one of the acceptable context roles for this element's role.
427
+
428
+ ### `aria-role-name-present`
429
+
430
+ **ARIA roles that require an accessible name have one**
431
+
432
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
433
+
434
+ Checks that the ARIA roles WAI-ARIA requires an accessible name for expose a non-empty one.
435
+
436
+ **Applies to.** Applies to elements whose role attribute is exactly one of grid, meter, progressbar, radiogroup or tree, and that are included in the accessibility tree. Membership is decided by WAI-ARIA's own "Accessible Name Required: True" characteristic, not by whether a role merely permits a name: tablist, toolbar, menu, menubar and scrollbar are name-from-author roles the spec does not require a name for, and are out of scope. meter and progressbar are also covered by meter-name-present and progressbar-name-present, which map to SC 1.1.1; this rule is what gives those two roles their 4.1.2 coverage.
437
+
438
+ **Expectation.** The element has a non-empty aria-label, an aria-labelledby that resolves to non-empty text, or a non-empty title. Every role in the set is name-from-author-only, so descendant text is not accepted: a labelled child inside a composite widget would otherwise pass the container that has no name of its own.
439
+
440
+ ### `aria-roles-valid`
441
+
442
+ **role attribute must be a valid, non-abstract ARIA role**
443
+
444
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
445
+
446
+ Checks that an explicit role="" attribute resolves to a real, non-abstract WAI-ARIA role.
447
+
448
+ **Applies to.** Applies to any element with a non-empty role="" attribute in the composed DOM.
449
+
450
+ **Expectation.**
451
+
452
+ At least one role token names a concrete, non-abstract ARIA role. Graded by what the element falls back to when none does:
453
+
454
+ - FAIL on a roleless host (div, span, custom element), which is left exposed as generic, so the role the author meant reaches no one.
455
+ - CANTTELL where the element has a native role (a <button>, <nav>, <a href>), which the accessibility tree keeps using. ACT 674b10 lists 4.1.2 as a secondary requirement only, "satisfied through the implicit role," so the bad token is worth reporting but is not itself the criterion failing.
456
+
457
+ ### `aria-text`
458
+
459
+ **role="text" elements should have no focusable descendants**
460
+
461
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
462
+
463
+ Checks that elements with role="text" contain no focusable descendant (link, button, form control, tabindex, iframe, or contenteditable).
464
+
465
+ **Applies to.** Elements with an explicit `role="text"`.
466
+
467
+ **Expectation.** `role="text"` tells assistive technology to treat an element's whole subtree as a single unit of plain text (e.g. text visually split across multiple `<span>`s by styling). Per the WAI-ARIA Authoring Practices, this only makes sense when that subtree contains no focusable content: a focusable descendant inside a "this is just text" region is unreachable or confusing for keyboard/AT users.
468
+
469
+ ### `aria-valid-attr`
470
+
471
+ **aria-* attributes must be real, defined ARIA attributes**
472
+
473
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
474
+
475
+ Checks that every aria-* attribute name present in the DOM is a real attribute defined by the WAI-ARIA specification.
476
+
477
+ **Applies to.** Applies to any element in the composed DOM that carries at least one attribute whose name starts with "aria-".
478
+
479
+ **Expectation.** Each aria-* attribute name is a real attribute defined by the WAI-ARIA specification (catches typos / made-up attribute names, which are silently ignored by assistive technology and therefore a real, deterministic defect). Reported at CANTTELL rather than FAIL: an aria-* attribute the spec does not define is inert, so nothing about the element's exposed name, role or value changes because it is there. Where the author meant a real attribute and the element ends up without a name, that absence is the naming rules' decision, not this one's. ACT 5f99a7 maps 1.3.1/4.1.2 as secondary requirements, "less strict" than the rule itself.
480
+
481
+ ### `aria-valid-attr-value`
482
+
483
+ **aria-* attribute values must match their declared type**
484
+
485
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
486
+
487
+ Checks that every recognized aria-* attribute has a value conforming to its WAI-ARIA-declared value type (boolean, tristate, token, integer, number, or ID reference).
488
+
489
+ **Applies to.** Applies to any element carrying at least one recognized aria-* attribute (unrecognized attribute names are aria-valid-attr's concern, not evaluated here).
490
+
491
+ **Expectation.** Each attribute's value conforms to its WAI-ARIA-declared value type: boolean ("true"/"false"), tristate ("true"/"false"/"mixed"), a token from a fixed enumerated set, an integer, a real number, or an ID reference (list) that resolves to an existing element in the document. Per ACT 6a7281's own applicability ("any state or property that is NOT empty"), an explicitly empty value, including a bare boolean-style attribute with no "=value" at all, e.g. `aria-checked` alone, is out of scope for every value type, not a violation: a common, deliberate pattern in templated markup (e.g. React conditionally rendering `aria-describedby={hasError ? errorId : ''}`).
492
+
493
+ ### `autocomplete-valid`
494
+
495
+ **autocomplete attribute must be a valid autofill value**
496
+
497
+ automatic · WCAG 1.3.5 (AA) · confidence high · default severity moderate
498
+
499
+ Checks that a non-empty autocomplete attribute is "on"/"off" or a well-formed autofill detail token list.
500
+
501
+ **Applies to.** Applies to form controls (input, select, textarea) with a non-empty autocomplete attribute.
502
+
503
+ **Expectation.** The value is "on"/"off" alone, or a well-formed autofill detail token list: an optional "section-*" token, then an optional "shipping"/"billing" token, then an optional contact-modality token (home/work/mobile/fax/pager/impp), then exactly one recognized field-name token (name, email, street-address, cc-number, tel, ...), optionally followed by "webauthn". A malformed value means the field is not reliably identified for assistive technology that relies on autocomplete to describe the expected input purpose.
504
+
505
+ ### `avoid-inline-spacing`
506
+
507
+ **Inline style must not force text spacing below the WCAG metric**
508
+
509
+ automatic · WCAG 1.4.12 (AA) · confidence high · default severity moderate
510
+
511
+ Checks that where inline style forces line-height, letter-spacing or word-spacing with !important, the value already meets WCAG 1.4.12, so the user has nothing left to override.
512
+
513
+ **Applies to.** Applies to a rendered element with visible text of its own whose style attribute declares line-height, letter-spacing or word-spacing as `!important` with a real value. A CSS-wide keyword (inherit, initial, unset, revert) specifies no spacing of its own and is out of scope.
514
+
515
+ **Expectation.** Each such declaration already meets WCAG 1.4.12's own metric for that property, as a multiple of the font size: line-height at least 1.5, letter-spacing at least 0.12, word-spacing at least 0.16. A forced value that already satisfies the criterion leaves the user nothing to override.
516
+
517
+ ### `binary-control-name-present`
518
+
519
+ **Binary controls have an accessible name**
520
+
521
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
522
+
523
+ Checks that checkbox, radio, and switch controls expose a non-empty accessible name.
524
+
525
+ **Applies to.** Applies to elements carrying role="checkbox", role="radio" or role="switch" (the attribute must name one of those roles alone, not a fallback list) that are included in the accessibility tree. A native <input type="checkbox">/<input type="radio"> is in scope only when it carries one of those roles explicitly; without a role attribute it belongs to form-control-programmatic-label-present.
526
+
527
+ **Expectation.** The control has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, or from title. A native checkbox or radio additionally accepts an associated <label>, the labels API, a wrapping <label>, or label[for], with at most four labels read for determinism, and any other element accepts its own subtree text, those roles being name-from-content.
528
+
529
+ ### `button-name-present`
530
+
531
+ **Buttons have an accessible name**
532
+
533
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
534
+
535
+ Checks that buttons expose a non-empty accessible name.
536
+
537
+ **Applies to.** Applies to <button>, <input type="button">, <input type="submit">, <input type="reset"> and elements with role="button", where the element is included in the accessibility tree. role="presentation"/"none" takes an element out of scope unless a global ARIA attribute or focusability restores its role, per presentational roles conflict resolution.
538
+
539
+ **Expectation.** The element has a non-empty accessible name. A programmatic name is taken first (aria-labelledby, aria-label, an associated <label>, title). Failing that, an <input> button falls back to its value attribute, and type="submit"/type="reset" fall back to the user agent's own "Submit"/"Reset" default, which is why those two are never nameless. Failing both, a button whose role is name-from-content falls back to its subtree text, counting each descendant's own name (an <img alt>, aria-label or title) rather than only text nodes.
540
+
541
+ ### `bypass-blocks-present`
542
+
543
+ **Page must provide a way to bypass repeated blocks**
544
+
545
+ manual · WCAG 2.4.1 (A) · confidence medium · default severity moderate
546
+
547
+ Checks that the page has at least one recognized WCAG 2.4.1 bypass-blocks mechanism: a main landmark, a working same-page anchor link, or a heading.
548
+
549
+ **Applies to.** Always applicable to any HTML document with a <body> element: "bypass blocks" is a whole-page concern, matching aria-hidden-body / page-title-present's pattern of evaluating the document directly rather than a scoped root.
550
+
551
+ **Expectation.** At least one of the following recognized WCAG 2.4.1 techniques is present: (a) a main landmark (<main> or [role="main"]), technique ARIA11: a screen reader user can jump straight to it, bypassing everything before it (nav, header, repeated blocks) in one step; (b) a working same-page anchor link, technique G1/G123: an <a href="#id"> (or legacy <a name="id">) whose target resolves to a real element in the link's own tree (light DOM or the same shadow root). Not required to be positioned before a <nav> or be keyboard-focus-order-first (see implementation notes); (c) at least one heading (<h1>-<h6> or [role="heading"]) that is both included in the accessibility tree AND visible (not off-screen, clipped, opacity:0, or zero-size-overflow-hidden), technique H69: heading navigation is itself a standards-recognized bypass mechanism (e.g. a screen reader's "jump by heading" command), but ACT 047fe0's own Expectation requires visibility too, since a screen-reader-only heading leaves sighted keyboard users with no equivalent way to locate the start of non-repeated content.
552
+
553
+ ### `canvas-text-alternative-present`
554
+
555
+ **<canvas> must provide a text alternative**
556
+
557
+ automatic · WCAG 1.1.1 (A) · confidence high · default severity serious
558
+
559
+ Checks that <canvas> elements provide a text alternative via fallback content or an accessible name.
560
+
561
+ **Applies to.** Applies to <canvas> elements included in the accessibility tree. Hidden elements are excluded whether or not they are focusable.
562
+
563
+ **Expectation.** Each applicable <canvas> provides a text alternative via fallback content or an accessible name.
564
+
565
+ ### `canvas-text-alternative-quality`
566
+
567
+ **<canvas> text alternative must be appropriate (manual review)**
568
+
569
+ manual · WCAG 1.1.1 (A) · confidence medium · default severity minor
570
+
571
+ Flags <canvas> elements with a detected text alternative for human review of equivalence and appropriateness.
572
+
573
+ **Applies to.** Applies to <canvas> elements that already carry a text alternative: fallback content inside the element, an ARIA name, or a title. A <canvas> with none of those has no alternative whose quality could be judged; that's canvas-text-alternative-present's failure. The element must be included in the accessibility tree, and role="presentation"/"none" takes it out of scope unless it is focusable, which restores its role.
574
+
575
+ **Expectation.** Human review is required to confirm that the provided text alternative is accurate and appropriate.
576
+
577
+ ### `combobox-name-present`
578
+
579
+ **Comboboxes have an accessible name**
580
+
581
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
582
+
583
+ Checks that elements with role="combobox" expose a non-empty accessible name.
584
+
585
+ **Applies to.** Applies to elements carrying role="combobox" (the attribute must name that role alone, not a fallback list) that are included in the accessibility tree. An element with the matching implicit role but no role attribute is out of scope.
586
+
587
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, or from title. role="combobox" is name-from-author-only, so subtree text is never accepted: text sitting inside a custom combobox widget is not reliably exposed as its name. On a labelable element (<input role="combobox">) an associated <label> counts as well.
588
+
589
+ ### `contrast-computable`
590
+
591
+ **Color contrast is computable for rendered text**
592
+
593
+ automatic · WCAG 1.4.3, 1.4.6 (AAA) · confidence high · default severity serious
594
+
595
+ Determines whether sufficient information is available to compute WCAG color contrast for visible text (e.g., no gradients/images/blend modes that make background indeterminate).
596
+
597
+ **Applies to.** Applies to every visible text node in scope, plus the label of <input type="button">/[type="submit"]/[type="reset"], which is rendered from the value attribute and so is invisible to a text-node walk. Text counts only when its element is DOM-visible under the run's visibility mode, is not clipped out of sight by the sr-only technique (clip or clip-path), and belongs neither to a disabled control nor to the label of one, WCAG's inactive-user-interface-component exception. Subtrees excluded via engineOptions.excludeSelectors are skipped, and open shadow roots are walked as roots in their own right.
598
+
599
+ **Expectation.** Both sides of the contrast calculation can be established from CSS for every applicable text node: an effective background resolving to an opaque color, and a parsable foreground color. Where either cannot be, a background image or gradient, mix-blend-mode, a filter or backdrop-filter, a text-shadow (which may add contrast this engine has no glyph-rendering model to account for), ancestor opacity, a root background that never becomes opaque, or a color that does not parse, the result is cantTell naming the blocker. This rule is the one that reports that uncertainty, which is what lets contrast-minimum and contrast-enhanced stay silent on the same text instead of guessing at a ratio.
600
+
601
+ ### `contrast-enhanced`
602
+
603
+ **Text meets enhanced color contrast (AAA)**
604
+
605
+ automatic · WCAG 1.4.6 (AAA) · confidence high · default severity serious
606
+
607
+ Checks that visible text has a contrast ratio of at least 7:1 (normal) or 4.5:1 (large), when contrast is computable from CSS.
608
+
609
+ **Applies to.** Applies to the visible text contrast-computable applies to, see that rule for the eligibility gates, narrowed to text whose background and foreground are actually computable. Eligible text that is not computable leaves this rule notApplicable rather than cantTell: reporting that uncertainty belongs to contrast-computable, so the two never report the same text twice.
610
+
611
+ **Expectation.** Every computable text node reaches the ratio SC 1.4.6 requires for its size: 4.5:1 for large text, 7:1 for everything else. Text is large at 24px or more, or at 14pt (about 18.667px) or more when the computed font weight is 700 or higher.
612
+
613
+ ### `contrast-minimum`
614
+
615
+ **Text meets minimum color contrast (AA)**
616
+
617
+ automatic · WCAG 1.4.3 (AA) · confidence high · default severity serious
618
+
619
+ Checks that visible text has a contrast ratio of at least 4.5:1 (normal) or 3:1 (large), when contrast is computable from CSS.
620
+
621
+ **Applies to.** Applies to the visible text contrast-computable applies to, see that rule for the eligibility gates, narrowed to text whose background and foreground are actually computable. Eligible text that is not computable leaves this rule notApplicable rather than cantTell: reporting that uncertainty belongs to contrast-computable, so the two never report the same text twice.
622
+
623
+ **Expectation.** Every computable text node reaches the ratio SC 1.4.3 requires for its size: 3:1 for large text, 4.5:1 for everything else. Text is large at 24px or more, or at 14pt (about 18.667px) or more when the computed font weight is 700 or higher.
624
+
625
+ ### `css-focus-indicator-suppressed`
626
+
627
+ **Focus indicator must not be removed without a replacement**
628
+
629
+ manual · WCAG 2.4.7 (AA) · confidence medium · default severity serious
630
+
631
+ Flags elements in the tab order whose focus outline is removed by a :focus/:focus-visible rule with no replacement indicator (border, box-shadow, background, ...) in any other focus rule matching them.
632
+
633
+ **Applies to.** Elements in sequential focus navigation (tabbable and rendered) on a page whose accessible stylesheets contain at least one `:focus` or `:focus-visible` rule. With no focus rule anywhere, every element keeps the user agent's own indicator and there is nothing to check.
634
+
635
+ **Expectation.** No element is matched by a `:focus`/`:focus-visible` rule that removes the outline (`outline: none`, `outline: 0`, `outline-color: transparent`, ...) unless some other focus rule matching it draws a replacement: a border, box-shadow, background, color change, a positive outline of its own, or a `::before`/`::after` decoration.
636
+
637
+ ### `css-hidden-focus`
638
+
639
+ **Focusable elements must not be visually hidden**
640
+
641
+ manual · WCAG 2.4.7 (AA) · confidence low · default severity serious
642
+
643
+ Checks that keyboard-focusable elements are not visually hidden by CSS techniques that can leave them in the tab order.
644
+
645
+ **Applies to.** Applies to elements that are tabbable (keyboard-focusable) but are visually hidden via CSS techniques that can leave them in the tab order.
646
+
647
+ **Expectation.**
648
+
649
+ No element should be tabbable while visually hidden (e.g., opacity:0, clipped, off-screen). Notes:
650
+
651
+ - This rule intentionally targets CSS techniques that *can* keep an element focusable.
652
+ - Elements removed from rendering (display:none, visibility:hidden, [hidden]) are excluded.
653
+ - The rule uses deterministic heuristics (computed style parsing) and does not rely on layout geometry.
654
+
655
+ ### `css-orientation-lock`
656
+
657
+ **CSS must not lock the page to a single orientation**
658
+
659
+ automatic · WCAG 1.3.4 (AA) · confidence high · default severity serious
660
+
661
+ Checks that no @media (orientation: portrait|landscape) rule sets a transform: rotate(...) on the page, a known technique for defeating device orientation.
662
+
663
+ **Applies to.** Any accessible (same-document, non-cross-origin) stylesheet, inline `<style>` blocks and same-origin `<link>` stylesheets already loaded into `document.styleSheets`.
664
+
665
+ **Expectation.** No `@media (orientation: portrait)` or `@media (orientation: landscape)` block sets a `transform`/`-webkit-transform`/`rotate` rotation of approximately 90 degrees (mod 180, i.e. ~90 or ~270), the well-known technique for visually forcing one orientation regardless of the device's actual orientation, which defeats WCAG 1.3.4's requirement that content not restrict its view to a single display orientation unless that orientation is essential.
666
+
667
+ ### `definition-list-children-valid`
668
+
669
+ **Description lists must be structured correctly**
670
+
671
+ automatic · WCAG 1.3.1 (A) · confidence high · default severity serious
672
+
673
+ Checks that <dl> elements only directly contain <dt>/<dd> groups (optionally wrapped in one <div>), <script>, <template>, or <style>.
674
+
675
+ **Applies to.** Applies to <dl> elements that have at least one direct element child.
676
+
677
+ **Expectation.** Every direct element child is <dt>, <dd>, <script>, <template>, <style>, or a <div> whose own children are drawn from that same set (a single level of wrapping div is allowed, matching how authors commonly group dt/dd pairs). If the flattened set contains any <dt> or <dd> at all, it must contain BOTH (an unbalanced dt-without-dd or dd-without-dt is invalid). A flattened set with neither is vacuously fine, not a violation (see implementation-notes). Any other direct or wrapped child breaks the description-list semantics assistive technologies rely on.
678
+
679
+ ### `deprecated-elements-not-used`
680
+
681
+ **Obsolete non-stoppable elements (<blink>, <marquee>) must not be used**
682
+
683
+ automatic · WCAG 2.2.2 (A) · confidence high · default severity serious
684
+
685
+ Checks that deprecated, non-standard HTML elements whose blinking/scrolling content cannot be paused, stopped, or hidden by the user (<blink>, <marquee>) are not present.
686
+
687
+ **Applies to.** Applies to any <blink> or <marquee> element present in scope. These are obsolete, non-standard HTML elements whose defining behavior (blinking or auto-scrolling text) has no built-in user mechanism to pause, stop, or hide it.
688
+
689
+ **Expectation.** Neither element is present. Since their movement can never be paused, stopped, or hidden by the user, presence is itself the violation. This rule has no partial-pass case (it reports only when the element is found).
690
+
691
+ ### `dialog-name-present`
692
+
693
+ **Dialogs have an accessible name**
694
+
695
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
696
+
697
+ Checks that elements with role="dialog" or role="alertdialog" expose a non-empty accessible name.
698
+
699
+ **Applies to.** Applies to elements carrying role="dialog" or role="alertdialog" (the attribute must name one of those roles alone, not a fallback list) that are included in the accessibility tree. A native <dialog> without an explicit role is out of scope.
700
+
701
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, or from title. Both roles are name-from-author-only, so the heading or body text inside the dialog is not accepted as its name unless aria-labelledby points at it.
702
+
703
+ ### `dlitem-parent-valid`
704
+
705
+ **Description-list items must be inside a description list**
706
+
707
+ automatic · WCAG 1.3.1 (A) · confidence high · default severity serious
708
+
709
+ Checks that <dt>/<dd> elements are contained by a <dl>, directly or via one wrapping <div>.
710
+
711
+ **Applies to.** Applies to <dt>/<dd> elements that have a parent element.
712
+
713
+ **Expectation.** The parent is <dl>, or the parent is a <div> whose own parent is <dl> (a single level of wrapping div is allowed, matching how authors commonly group dt/dd pairs). A <dt>/<dd> used outside a real description-list container is not exposed as a term/definition to assistive technologies.
714
+
715
+ ### `duplicate-id`
716
+
717
+ **IDs must be unique**
718
+
719
+ automatic · WCAG 4.1.1 (A) · confidence high · default severity moderate
720
+
721
+ Checks that every non-empty id attribute value is unique within its own document or shadow tree (WCAG 2.0/2.1 SC 4.1.1, removed in WCAG 2.2).
722
+
723
+ **Applies to.** Applies to any element carrying a non-empty id attribute. Visibility is irrelevant. A duplicate id breaks the same lookups whether the element renders or not, which is why ACT 3ea0c8 evaluates hidden elements too.
724
+
725
+ **Expectation.** No other element in the same tree carries the same id value. Ids are scoped per document tree and per shadow tree, so the same id inside two different shadow roots is not a duplicate.
726
+
727
+ ### `duplicate-id-aria`
728
+
729
+ **IDs referenced by ARIA must be unique**
730
+
731
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
732
+
733
+ Checks that any id value referenced by an ARIA ID-reference attribute (aria-labelledby, aria-describedby, aria-owns, aria-controls, aria-activedescendant, aria-flowto, aria-errormessage, aria-details) is unique in the document.
734
+
735
+ **Applies to.** Applies when the document contains at least one non-empty aria-labelledby, aria-describedby, aria-owns, aria-controls, aria-activedescendant, aria-flowto, aria-errormessage, or aria-details attribute (i.e. at least one ARIA ID reference exists to resolve).
736
+
737
+ **Expectation.** For every id value referenced by one of those attributes, exactly one element in the document carries that id. A duplicate does not break the reference: it resolves to the first element in tree order, so the name is still computed. Whether that element is the intended target depends on author intent, which markup does not carry, so the outcome is cantTell.
738
+
739
+ ### `embed-text-alternative-present`
740
+
741
+ **<embed> must provide a text alternative**
742
+
743
+ automatic · WCAG 1.1.1 (A) · confidence high · default severity serious
744
+
745
+ Checks that <embed> elements provide a text alternative via an accessible name.
746
+
747
+ **Applies to.** Applies to <embed> elements that are exposed to assistive technologies. Elements otherwise hidden from the accessibility tree remain applicable if they are tabbable or referenced by IDREF relationships (per engine eligibility checks). role="presentation"/role="none" are excluded only when not focusable.
748
+
749
+ **Expectation.**
750
+
751
+ Each applicable <embed> provides a text alternative via:
752
+
753
+ - an accessible name (aria-labelledby/aria-label), OR
754
+ - a title attribute (best-effort fallback).
755
+
756
+ Note: <embed> does not support fallback content in HTML, so this rule does not check children.
757
+
758
+ ### `embed-text-alternative-quality`
759
+
760
+ **<embed> text alternative must be appropriate (manual review)**
761
+
762
+ manual · WCAG 1.1.1 (A) · confidence medium · default severity minor
763
+
764
+ Flags <embed> elements with a detected name for human review of appropriateness.
765
+
766
+ **Applies to.** Applies to <embed> elements that already carry a text alternative: a non-empty aria-label, an aria-labelledby that resolves to non-empty text, or a non-empty title. An aria-labelledby pointing at a missing id resolves to nothing and so is not a text alternative to review; that element is embed-text-alternative-present's failure. The element must be included in the accessibility tree, and role="presentation"/"none" takes it out of scope unless it is focusable, which restores its role.
767
+
768
+ **Expectation.** Human review is required to confirm that the provided text alternative is accurate and appropriate.
769
+
770
+ ### `empty-heading`
771
+
772
+ **Headings must not be empty**
773
+
774
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
775
+
776
+ Checks that heading elements (<h1>-<h6> or role="heading") have a non-empty accessible name.
777
+
778
+ **Applies to.** Applies to elements with a heading role: native <h1>-<h6>, or any element with explicit role="heading" (unless overridden by another explicit role).
779
+
780
+ **Expectation.** The heading has a non-empty accessible name: aria-label, aria-labelledby, visible text content not hidden from assistive technology, or (as a last resort) a title attribute. An empty heading is announced as "heading, level N" with nothing else, which is confusing when navigating by heading.
781
+
782
+ ### `empty-table-header`
783
+
784
+ **Table header cells must not be empty**
785
+
786
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
787
+
788
+ Checks that table header cells (<th>, or any element with role="columnheader"/"rowheader") have visible text content. A header named only via aria-label/aria-labelledby is also flagged, since real screen-reader/browser support for that is inconsistent.
789
+
790
+ **Applies to.** Applies to <th> elements that don't carry a conflicting explicit role, plus any element (native <th> or not) with role="columnheader" or role="rowheader" (`th:not([role]), [role="columnheader"], [role="rowheader"]`): a <th> that explicitly restates role="columnheader"/"rowheader" is still covered via the second clause, but a <th role="presentation"> (no longer meaningfully a header) is correctly excluded, and an ARIA-role-only header (e.g. a <div role="columnheader"> in a role="grid"/role="table" widget) is caught too.
791
+
792
+ **Expectation.** The header cell has visible text content. A <th> named only via aria-label/aria-labelledby (no visible text) is ALSO flagged, not treated as equivalent: aria-label support on <th> is inconsistent in practice. NVDA+Firefox and iOS VoiceOver+Safari ignore it entirely (only visible text is announced), JAWS+Chrome/IE11 also only announce visible text in the header cell itself. Visible text is the one mechanism confirmed to work across every tested combination. See https://html5accessibility.com/stuff/2024/05/22/not-so-short-note-on-aria-label-usage-big-table-edition/.
793
+
794
+ ### `focus-order-semantics`
795
+
796
+ **Elements added to the tab order should have interactive semantics**
797
+
798
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
799
+
800
+ Flags elements with tabindex >= 0 whose explicit role is a non-interactive structural/document role (e.g. heading, list, region, presentation), for manual review.
801
+
802
+ **Applies to.** Elements with an explicit `tabindex` of `0` or greater (in the tab order) AND an explicit `role` attribute that is one of a curated set of clearly non-interactive, structural/document roles.
803
+
804
+ **Expectation.** An element placed in the tab order on purpose should communicate why it's focusable: a role like `heading`, `list`, `region`, or `presentation` gives assistive technology no interactive semantic to announce, which is confusing for keyboard users who land on it and get no indication of what activating it (if anything) would do.
805
+
806
+ ### `form-control-label-quality`
807
+
808
+ **Form field labels should be descriptive and distinguishable**
809
+
810
+ manual · WCAG 2.4.6 (AA) · confidence medium · default severity minor
811
+
812
+ Flags a visible form-field label that is a placeholder ("Label", "Field"), or that repeats another field's label with no visible context (heading, legend, or row) telling the two apart.
813
+
814
+ **Applies to.** Visible form fields: native `input` (excluding hidden and the button-like types), `select`, `textarea`, or an element with one of the ARIA widget roles ACT cc0f0a lists (checkbox, combobox, listbox, menuitemcheckbox, menuitemradio, radio, searchbox, slider, spinbutton, switch, textbox) that carry a visible programmatic label: a `<label>` association, or the elements `aria-labelledby` points at. A field named only by `aria-label`/`title` has no visible label to judge and is out of scope here (its labelling mechanism is `form-control-programmatic-label-quality`'s concern, its presence `form-control-programmatic-label-present`'s).
815
+
816
+ **Expectation.** The visible label text (a) is not a placeholder left in the markup ("label", "field", "enter text", ...), (b) is not shared with another field that no visible context tells apart (the same "Name" twice, with nothing visible on screen saying which is shipping and which is billing), and (c) is the whole of the field's programmatic label, not the visible fragment of a label whose descriptive part is hidden.
817
+
818
+ ### `form-control-programmatic-label-present`
819
+
820
+ **Form controls must have a programmatic label**
821
+
822
+ automatic · WCAG 1.3.1, 3.3.2, 4.1.2 (A) · confidence medium · default severity serious
823
+
824
+ Checks that form controls have a programmatic label via <label>, aria-label, aria-labelledby, title, or placeholder.
825
+
826
+ **Applies to.** Applies to <input>, <select> and <textarea> elements included in the accessibility tree, excluding the input types hidden, submit, reset, button and image, which take their name from a value or alt attribute rather than from a label. A control carrying an explicit ARIA widget role is out of scope, button, checkbox, combobox, listbox, textbox, slider and the rest of ROLE_OWNED_ELSEWHERE each have a naming rule of their own, and role="presentation"/"none" removes a control unless it is still tabbable.
827
+
828
+ **Expectation.** Each applicable control carries a programmatic label by one of the mechanisms helpers.getLabelMethod resolves, in its priority order: an associated <label>, aria-labelledby, aria-label, title, then placeholder. Any of the five satisfies this rule. Whether the weaker two are an appropriate primary label is a separate question, asked by form-control-programmatic-label-quality.
829
+
830
+ ### `form-control-programmatic-label-quality`
831
+
832
+ **Form controls should not rely on placeholder or title as the primary label**
833
+
834
+ manual · WCAG 4.1.2 (A) · confidence medium · default severity moderate
835
+
836
+ Flags form controls whose computed accessible name relies on placeholder or title as the primary labeling method. Prefer <label> or aria-labelledby.
837
+
838
+ **Applies to.**
839
+
840
+ Applies to labelable native form controls exposed to assistive technologies:
841
+
842
+ - input (excluding type=hidden|submit|reset|button|image)
843
+ - select
844
+ - textarea
845
+
846
+ role="presentation"/"none" are excluded only when not focusable.
847
+
848
+ **Expectation.**
849
+
850
+ If a control has a programmatic name, it should not rely ONLY on:
851
+
852
+ - placeholder (non-empty)
853
+ - title (non-empty)
854
+
855
+ Prefer an associated <label> or aria-labelledby.
856
+
857
+ ### `form-control-single-label`
858
+
859
+ **Form controls must not have multiple labels**
860
+
861
+ automatic · WCAG 3.3.2 (A) · confidence high · default severity moderate
862
+
863
+ Checks that a form control is associated with at most one <label> (by wrapping or by label[for]).
864
+
865
+ **Applies to.** Applies to labelable form controls (input, excluding hidden/submit/reset/button/image; select; textarea).
866
+
867
+ **Expectation.**
868
+
869
+ At most one <label> that can contribute to the control's accessible name is associated with it, by wrapping it, or by a <label for="..."> on its id (a label that both wraps and self-references via for counts once). Graded by whether the surplus labels actually compete for the name:
870
+
871
+ - PASS when an override (aria-labelledby / aria-label) supersedes every native <label>: the labels then contribute nothing to the name, so they cannot be ambiguous. A visible-label-vs-name mismatch is SC 2.5.3 Label in Name's concern, not this rule's.
872
+ - FAIL when two or more non-empty labels compete and there is no override: screen readers announce a non-deterministic subset.
873
+ - CANTTELL when one non-empty label is joined by empty label association(s) and there is no override: the name usually resolves to the real label, but handling of the empty association is not guaranteed across user agents.
874
+
875
+ All-empty associations with no override are a missing-name case (the sibling rule below), not an ambiguity, so this rule stays silent.
876
+
877
+ ### `heading-order`
878
+
879
+ **Heading levels must not skip a level**
880
+
881
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
882
+
883
+ Checks that heading levels increase by at most one at a time in document order.
884
+
885
+ **Applies to.** Applies whenever the page contains two or more heading elements (native <h1>-<h6>, or explicit role="heading" with aria-level; default level 2 per the ARIA spec when aria-level is absent/invalid).
886
+
887
+ **Expectation.** In document order, each heading's level is no more than one greater than the highest heading level seen so far. Jumping deeper by more than one level (e.g. an <h1> followed directly by an <h3>, skipping <h2>) breaks the document outline assistive technology users rely on when navigating by heading. Going back to a shallower level at any point is always fine.
888
+
889
+ ### `heading-quality`
890
+
891
+ **Heading text should be descriptive, not a placeholder**
892
+
893
+ manual · WCAG 2.4.6 (AA) · confidence medium · default severity minor
894
+
895
+ Flags headings whose accessible name is a placeholder rather than a description of the content that follows: a generic word ("Heading", "Untitled"), a numbered template slot ("Section 2"), a filename, or a URL.
896
+
897
+ **Applies to.** Elements with a heading role (native <h1>-<h6>, or any element with an explicit role="heading") that are included in the accessibility tree and have a non-empty accessible name. A heading with no name at all is `empty-heading`'s concern, not this rule's.
898
+
899
+ **Expectation.** The heading's accessible name, normalized (whitespace-collapsed, case-folded, trailing punctuation stripped), is not a placeholder left in the markup: a known generic word ("heading", "title", "untitled", "lorem ipsum", ...), a numbered template slot ("Heading 2", "Section 3"), a filename, or a URL. None of these describe the topic or purpose of the content they introduce.
900
+
901
+ ### `html-lang-attr-present`
902
+
903
+ **Page language is declared**
904
+
905
+ automatic · WCAG 3.1.1 (A) · confidence high · default severity serious
906
+
907
+ Checks that the default language of the page is programmatically declared.
908
+
909
+ **Applies to.** Applies to HTML documents with a root <html> element. The rule evaluates the document element only and does not iterate over child nodes. Non-HTML documents or documents without a document element are not applicable.
910
+
911
+ **Expectation.** The <html> element has a lang attribute. The lang attribute is not empty and contains a syntactically valid language tag.
912
+
913
+ ### `html-xml-lang-mismatch`
914
+
915
+ **lang and xml:lang must not disagree**
916
+
917
+ automatic · WCAG 3.1.1 (A) · confidence high · default severity serious
918
+
919
+ Checks that the <html> element's lang and xml:lang attributes declare the same primary language, when both are present.
920
+
921
+ **Applies to.** Applies when the <html> element has both a non-empty lang attribute and a non-empty xml:lang attribute.
922
+
923
+ **Expectation.** The primary language subtag (the part before the first "-") of lang and xml:lang match, case-insensitively. When both attributes are present but declare different languages, assistive technology and user agents may resolve the page's language inconsistently.
924
+
925
+ ### `identical-iframes-same-purpose`
926
+
927
+ **Frames with the same name embed the same resource**
928
+
929
+ automatic · WCAG 4.1.2 (A) · confidence medium · default severity moderate
930
+
931
+ Checks that <iframe>/<frame> elements sharing an accessible name embed the same resource, since one name can only describe one resource.
932
+
933
+ **Applies to.** Applies to each set of two or more <iframe>/<frame> elements that are included in the accessibility tree and share the same non-empty accessible name, compared with whitespace collapsed. A frame named only by a mechanism that names nothing, or hidden from the accessibility tree, is not part of a set; a set needs two surviving members to exist at all.
934
+
935
+ **Expectation.** Every frame in a set resolves to the same resource. A shared name describes one resource, so two frames answering to it must embed the same one.
936
+
937
+ ### `identical-links-same-purpose`
938
+
939
+ **Links with the same accessible name should lead to the same destination**
940
+
941
+ manual · WCAG 2.4.9 (AAA) · confidence low · default severity minor
942
+
943
+ Flags groups of links that share the same accessible name but resolve to more than one distinct destination, for manual review of whether they serve the same purpose.
944
+
945
+ **Applies to.** Any `a[href]` or `[role="link"]` with a non-empty accessible name, grouped by that name (trimmed, whitespace-collapsed, case-folded).
946
+
947
+ **Expectation.** Within a page, links that share the same accessible name are expected to serve the same purpose (i.e. resolve to the same destination, the full resolved URL, including any fragment). Same-text-different- destination links are common and frequently intentional in real sites (e.g. repeated "Read more" links per article card), so this is authored as `type: 'manual'` (cantTell-capped, never fail) rather than a hard fail, flagging a real name/destination mismatch for human judgment instead of guessing intent.
948
+
949
+ ### `iframe-focusable-content`
950
+
951
+ **Frames with tabindex="-1" must not contain focusable content**
952
+
953
+ automatic · WCAG 2.1.1 (A) · confidence high · default severity moderate
954
+
955
+ Checks that same-origin <iframe>/<frame> elements with tabindex="-1" do not contain focusable content, since browsers do not propagate that restriction into the frame’s embedded document.
956
+
957
+ **Applies to.** Applies to <iframe>/<frame> elements with an explicit negative tabindex, whose embedded document is same-origin and reachable via contentDocument (cross-origin/unreachable frames assert nothing, see implementation notes).
958
+
959
+ **Expectation.** The frame's embedded document contains no focusable element. Browsers do not propagate tabindex="-1" on the host <iframe> into its embedded document: Tab can still reach focusable content inside, even though the frame itself is skipped. An author who set tabindex="-1" intending to remove the frame from the tab order has not actually done so if the embedded document contains focusable content. Exception: an iframe with both a `width` and `height` HTML attribute of 2px or less (a common "tracking pixel" pattern) cannot render any perceptible content, so focusable content inside it never satisfies ACT akn7bn's "visible" requirement and doesn't count.
960
+
961
+ ### `iframe-name-present`
962
+
963
+ **Frames have an accessible name**
964
+
965
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
966
+
967
+ Checks that <iframe>/<frame> elements expose a non-empty accessible name via aria-label, aria-labelledby, or the title attribute.
968
+
969
+ **Applies to.** Applies to <iframe>/<frame> elements that are eligible for the accessibility tree (isAccTreeEligible) AND reachable by sequential focus navigation, both conditions required, regardless of role.
970
+
971
+ **Expectation.** The element has a non-empty accessible name via aria-labelledby, aria-label, or the title attribute. Unlike most interactive elements, an iframe's name is never derived from its rendered content (the embedded document is a separate browsing context), this mirrors dialog-name-present's "name-from-author-only" reasoning.
972
+
973
+ ### `iframe-title-unique`
974
+
975
+ **Frame titles must be unique**
976
+
977
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity moderate
978
+
979
+ Checks that no two <iframe>/<frame> elements in scope share the same title attribute value.
980
+
981
+ **Applies to.** Applies to <iframe>/<frame> elements that carry a non-empty title attribute.
982
+
983
+ **Expectation.** No two frames in scope share the same (trimmed, case-sensitive) title attribute value, a duplicate title prevents assistive technology users from telling frames apart when scanning by name.
984
+
985
+ ### `image-redundant-alt`
986
+
987
+ **Image alt text must not duplicate adjacent visible text**
988
+
989
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
990
+
991
+ Checks that an <img> alt text is not identical to other visible text already present in its immediate parent element.
992
+
993
+ **Applies to.** Applies to <img> elements with non-empty alt text whose immediate parent element also has other visible text content (i.e. text nodes besides the image itself, commonly an <a> or <button> wrapping both an icon image and a text label).
994
+
995
+ **Expectation.** The image's alt text is not the same (case-insensitive, normalized) as the other visible text already in the same parent. When both are present, assistive technology announces the same words twice for a single control (e.g. an icon-plus-text link where the icon's alt duplicates the link text).
996
+
997
+ ### `img-alt-decorative`
998
+
999
+ **Excluded <img>/<canvas>/<svg> must be decorative (manual review)**
1000
+
1001
+ manual · WCAG 1.1.1 (A) · confidence medium · default severity minor
1002
+
1003
+ Flags <img>, <canvas> and <svg> elements excluded from the accessibility tree (aria-hidden, role="none"/"presentation", empty alt, or an unlabeled svg/canvas) for human review that they are purely decorative.
1004
+
1005
+ **Applies to.** Applies to visible <img>, <canvas> or <svg> elements excluded from the accessibility tree by any of: an aria-hidden ancestor-or-self, an explicit role="none"/"presentation" not overridden by focusability, an <img alt=""> (the native decorative marker, same focusability override), an unlabeled <svg> whose implicit role is graphics-document (no img/graphics-symbol role restatement, aria-name, <title>/<desc>, or focusability), or an unlabeled <canvas> with no explicit role at all. Per ACT e88epe, an element is skipped entirely when any ancestor already has an author-supplied name (aria-label, aria-labelledby, title, or an associated <label>), that ancestor's name is what matters, not this element's exclusion (the common real case: an icon-only button already named via aria-label).
1006
+
1007
+ **Expectation.** Human review is required to confirm the excluded element is purely decorative and conveys no information a user would otherwise miss.
1008
+
1009
+ ### `img-alt-present`
1010
+
1011
+ **<img> must have an alt attribute**
1012
+
1013
+ automatic · WCAG 1.1.1 (A) · confidence high · default severity serious
1014
+
1015
+ Checks that <img> elements provide an alt attribute to support a text alternative mechanism.
1016
+
1017
+ **Applies to.** Applies to <img> elements included in the accessibility tree. Images with role="presentation" or role="none" are excluded only when they are not focusable, since a focusable one reverts to the img role under presentational roles conflict resolution. Hidden images are excluded whether or not they are focusable.
1018
+
1019
+ **Expectation.** Each applicable <img> element has an alt attribute. The alt attribute may be empty (alt="").
1020
+
1021
+ ### `img-alt-quality`
1022
+
1023
+ **<img> alt text must be appropriate (manual review)**
1024
+
1025
+ manual · WCAG 1.1.1 (A) · confidence medium · default severity minor
1026
+
1027
+ Flags <img> elements with non-empty alt text for human review of appropriateness.
1028
+
1029
+ **Applies to.** Applies to <img> elements whose alt attribute is present and non-empty. The element must be included in the accessibility tree, and role="presentation"/"none" takes it out of scope unless it is focusable, which restores its role. An <img> with no alt at all is img-alt-present's failure, and one with alt="" is img-alt-decorative's review.
1030
+
1031
+ **Expectation.** Human review is required to confirm that the provided text alternative is accurate and appropriate.
1032
+
1033
+ ### `input-image-alt-decorative`
1034
+
1035
+ **<input type="image"> with alt="" must be appropriate (manual review)**
1036
+
1037
+ manual · WCAG 1.1.1 (A) · confidence medium · default severity minor
1038
+
1039
+ Flags <input type="image"> elements with empty alt for human review (usually not appropriate for functional controls).
1040
+
1041
+ **Applies to.** Applies to <input type="image"> elements whose alt attribute is present but empty once trimmed, and which still carry a name from another source: an ARIA name resolving to non-empty text, or a title. Without that other name there is nothing to weigh the empty alt against, and the control is input-image-alt-present's failure instead. The element must be included in the accessibility tree, and role="presentation"/"none" takes it out of scope unless it is focusable, which restores its role.
1042
+
1043
+ **Expectation.** Human review is required to confirm that the provided text alternative is accurate and appropriate.
1044
+
1045
+ ### `input-image-alt-present`
1046
+
1047
+ **<input type="image"> must have an alt attribute**
1048
+
1049
+ automatic · WCAG 1.1.1 (A) · confidence high · default severity serious
1050
+
1051
+ Checks that <input type="image"> elements provide an alt attribute to support a text alternative mechanism.
1052
+
1053
+ **Applies to.** Applies to <input type="image"> elements included in the accessibility tree.
1054
+
1055
+ **Expectation.** Each applicable <input type="image"> element has an alt attribute, and its accessible name is not the browser default for an image button. The alt attribute may be empty (alt="").
1056
+
1057
+ ### `input-image-alt-quality`
1058
+
1059
+ **<input type="image"> alt text must be appropriate (manual review)**
1060
+
1061
+ manual · WCAG 1.1.1 (A) · confidence medium · default severity minor
1062
+
1063
+ Flags <input type="image"> elements with non-empty alt text for human review of appropriateness.
1064
+
1065
+ **Applies to.** Applies to <input type="image"> elements whose alt attribute is present and non-empty: an image button whose alt is its label. The element must be included in the accessibility tree, and role="presentation"/"none" takes it out of scope unless it is focusable, which restores its role.
1066
+
1067
+ **Expectation.** Human review is required to confirm that the provided text alternative is accurate and appropriate.
1068
+
1069
+ ### `label-in-name`
1070
+
1071
+ **Label in Name: accessible name contains visible text**
1072
+
1073
+ automatic · WCAG 2.5.3 (A) · confidence high · default severity serious
1074
+
1075
+ Checks that when a control has a visible text label, the accessible name contains that visible label text (WCAG 2.5.3).
1076
+
1077
+ **Applies to.** Applies to controls that carry aria-label or aria-labelledby, are visually rendered, and have visible label text this engine can extract deterministically, from an associated <label>, from the control's own rendered text, or from the elements aria-labelledby points at. The candidates are <button>, <a href>, <summary>, non-hidden form controls, and the button, link, checkbox, radio, switch, searchbox, tab, menuitem, menuitemcheckbox, menuitemradio, option, treeitem and gridcell roles, minus anything hidden or disabled. aria-hidden is not excluded: it changes nothing about what is rendered on screen, which is what this SC is about.
1078
+
1079
+ **Expectation.** The accessible name contains the visible label's words, adjacent and in order. The comparison is over words rather than characters: parenthesised text is dropped, case is folded, text is NFKD-normalised, and every non-letter/digit becomes a separator, so punctuation and spacing differences never decide the outcome. Four shapes markup cannot settle are reported as cantTell instead of fail: a word hyphenated differently in the two places; a visible word the author may have abbreviated, marked by its trailing period; visible text rendered through a known icon font (the DOM text is real words, but nothing readable actually renders); and a whole visible label of exactly one character that doesn't even appear inside the accessible name, which per ACT 2ee8b8 may be "non-text content" (e.g. "X" meaning "close") rather than literal text.
1080
+
1081
+ ### `label-title-only`
1082
+
1083
+ **Form controls should not use title as their only label**
1084
+
1085
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1086
+
1087
+ Checks that a form control with a title attribute also has a real label (label element, aria-label, or aria-labelledby).
1088
+
1089
+ **Applies to.** Applies to labelable form controls (input, excluding hidden/submit/reset/button/image; select; textarea) that have a non-empty title attribute.
1090
+
1091
+ **Expectation.** The control also has a real label (a wrapping/associated <label>, aria-label, or aria-labelledby), rather than depending on the title attribute alone. A title-only tooltip is not reliably exposed by all assistive technology and is not visible at all until hover/focus, unlike a persistent visible label.
1092
+
1093
+ ### `landmark-banner-is-top-level`
1094
+
1095
+ **Banner landmark must be top-level**
1096
+
1097
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1098
+
1099
+ Checks that the banner landmark (role="banner" or a non-nested <header>) is not nested inside another landmark region.
1100
+
1101
+ **Applies to.** Applies whenever the page contains at least one banner candidate: explicit role="banner", OR a <header> with NO role attribute at all, regardless of nesting (see implementation notes on why candidate selection is unconditional on purpose).
1102
+
1103
+ **Expectation.** No banner candidate has an ancestor that is itself any landmark region. A banner nested inside another landmark is not a top-level, whole-page banner and confuses landmark-based navigation for assistive technology users.
1104
+
1105
+ ### `landmark-complementary-is-top-level`
1106
+
1107
+ **Complementary landmark must be top-level**
1108
+
1109
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1110
+
1111
+ Checks that the complementary landmark (role="complementary" or an <aside> that keeps its implicit role) is not nested inside another landmark region.
1112
+
1113
+ **Applies to.** Applies whenever the page contains at least one element carrying the complementary role: explicit role="complementary", or an <aside> that keeps its implicit role (see implementation notes on when it does not).
1114
+
1115
+ **Expectation.** No complementary candidate has an ancestor that is itself a landmark region. Complementary content supports the main content of the page and sits beside it; nested inside another landmark it is a section of that landmark instead, which is not what landmark navigation announces.
1116
+
1117
+ ### `landmark-contentinfo-is-top-level`
1118
+
1119
+ **Contentinfo landmark must be top-level**
1120
+
1121
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1122
+
1123
+ Checks that the contentinfo landmark (role="contentinfo" or a non-nested <footer>) is not nested inside another landmark region.
1124
+
1125
+ **Applies to.** Applies whenever the page contains at least one contentinfo candidate: explicit role="contentinfo", OR a <footer> with NO role attribute at all, regardless of nesting (see implementation notes on why candidate selection is unconditional on purpose).
1126
+
1127
+ **Expectation.** No contentinfo candidate has an ancestor that is itself any landmark region. A contentinfo nested inside another landmark is not a top-level, whole-page footer region and confuses landmark-based navigation for assistive technology users.
1128
+
1129
+ ### `landmark-main-is-top-level`
1130
+
1131
+ **Main landmark must be top-level**
1132
+
1133
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1134
+
1135
+ Checks that the main landmark (role="main" or <main>) is not nested inside another landmark region.
1136
+
1137
+ **Applies to.** Applies whenever the page contains at least one main landmark (explicit role="main", or an implicit <main> element).
1138
+
1139
+ **Expectation.** No main landmark has an ancestor that is itself any landmark region. A main region nested inside another landmark is not a top-level, whole-page main content area and confuses landmark-based navigation for assistive technology users.
1140
+
1141
+ ### `landmark-no-duplicate-banner`
1142
+
1143
+ **Page must not have more than one banner landmark**
1144
+
1145
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1146
+
1147
+ Checks that at most one banner landmark (role="banner" or a non-nested <header>) exists on the page.
1148
+
1149
+ **Applies to.** Applies whenever the page contains at least one banner landmark (explicit role="banner", or an implicit, non-nested <header>; see landmark-banner-is-top-level's implementation notes for the shared landmark-detection model).
1150
+
1151
+ **Expectation.** At most one banner landmark exists on the page. Per WAI-ARIA Authoring Practices, the banner landmark represents site-oriented content that identifies the page as a whole, so having more than one is ambiguous for assistive technology users navigating by landmark.
1152
+
1153
+ ### `landmark-no-duplicate-contentinfo`
1154
+
1155
+ **Page must not have more than one contentinfo landmark**
1156
+
1157
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1158
+
1159
+ Checks that at most one contentinfo landmark (role="contentinfo" or a non-nested <footer>) exists on the page.
1160
+
1161
+ **Applies to.** Applies whenever the page contains at least one contentinfo landmark (explicit role="contentinfo", or an implicit, non-nested <footer>).
1162
+
1163
+ **Expectation.** At most one contentinfo landmark exists on the page, mirroring landmark-no-duplicate-banner's rationale for contentinfo.
1164
+
1165
+ ### `landmark-no-duplicate-main`
1166
+
1167
+ **Page must not have more than one main landmark**
1168
+
1169
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1170
+
1171
+ Checks that at most one main landmark (role="main" or <main>) exists on the page.
1172
+
1173
+ **Applies to.** Applies whenever the page contains at least one main landmark (explicit role="main", or an implicit <main>).
1174
+
1175
+ **Expectation.** At most one main landmark exists on the page. Distinct, atomic decision from landmark-one-main (that rule flags zero mains too; this one only flags more than one).
1176
+
1177
+ ### `landmark-one-main`
1178
+
1179
+ **Page should have a main landmark**
1180
+
1181
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1182
+
1183
+ Checks that the page has at least one main landmark (role="main" or <main>).
1184
+
1185
+ **Applies to.** Always applicable to any HTML document with a <body> element: "does the page have a main landmark" is a whole-page concern, matching bypass-blocks-present's pattern of evaluating the document directly.
1186
+
1187
+ **Expectation.** At least one main landmark (role="main" or <main>), exposed to assistive technology, exists on the page. A page with none gives AT users no landmark to jump straight to for the primary content.
1188
+
1189
+ ### `landmark-unique`
1190
+
1191
+ **Landmarks with the same role must have unique names**
1192
+
1193
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1194
+
1195
+ Checks that when two or more landmarks share the same role, each has a distinct accessible name.
1196
+
1197
+ **Applies to.** Applies whenever two or more landmark regions on the page share the same landmark role (banner, contentinfo, main, navigation, complementary, region, form, or search; see implementation notes for the detection model).
1198
+
1199
+ **Expectation.** Among landmarks sharing a role, each has a distinct accessible name (via aria-label/aria-labelledby; landmarks are not named from content). Two same-role landmarks with the same name (including two both left unnamed) are indistinguishable to assistive technology users navigating by landmark.
1200
+
1201
+ ### `link-in-text-block`
1202
+
1203
+ **Links in text blocks must be distinguishable from surrounding text without relying on color alone**
1204
+
1205
+ automatic · WCAG 1.4.1 (A) · confidence high · default severity serious
1206
+
1207
+ Checks that a link inside a run of text is visually distinguishable from the surrounding text by underline, a font-weight/style difference, or a sufficient (>=3:1) color-contrast difference, not by color alone.
1208
+
1209
+ **Applies to.** Applies to <a href> elements whose immediate parent element also has at least one direct-child text node with non-whitespace content (i.e. the link sits inline within a run of plain text, not as a standalone item, e.g. not the sole content of a <li> nav item).
1210
+
1211
+ **Expectation.**
1212
+
1213
+ A link inside a text block must be visually distinguishable from the surrounding text by at least one non-color means:
1214
+
1215
+ - text-decoration: underline, OR
1216
+ - a different font-weight than the surrounding text, OR
1217
+ - a different font-style than the surrounding text, OR
1218
+ - a contrast ratio of at least 3:1 between the link's text color and the surrounding text's color (WCAG technique G183's threshold, sufficient contrast alone is an accepted alternative to underline).
1219
+
1220
+ Fails only when none of the above hold AND the color contrast between link and surrounding text is confidently computable and below 3:1, i.e. color is demonstrably the only cue.
1221
+
1222
+ ### `link-name-present`
1223
+
1224
+ **Links have an accessible name**
1225
+
1226
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1227
+
1228
+ Checks that links expose a non-empty accessible name.
1229
+
1230
+ **Applies to.** Applies to <a href>, <area href> and elements with role="link" that are included in the accessibility tree. An <a> without an href is not a link and is not matched.
1231
+
1232
+ **Expectation.** The element has a non-empty accessible name. A programmatic name is taken first (aria-labelledby, aria-label, an associated <label>, title), and failing that the element falls back to its own subtree text, counting each descendant's own name (an <img alt>, aria-label or title), the shape behind the common <a><img alt="..."></a> logo link. The content fallback is suppressed when an explicit, known role that is not name-from-content is present; an unrecognized role token falls back to the implicit role.
1233
+
1234
+ ### `link-name-quality`
1235
+
1236
+ **Link text should be descriptive, not generic**
1237
+
1238
+ manual · WCAG 2.4.4 (A) · confidence medium · default severity minor
1239
+
1240
+ Flags links whose full accessible name is a known non-descriptive phrase (e.g. "click here", "read more", "more") or a bare file-format name (e.g. "HTML", "PDF") with no adjacent context naming what it leads to, for manual review of whether the purpose is clear.
1241
+
1242
+ **Applies to.** Elements matching `a[href], area[href], [role="link"]` with a non-empty computed accessible name (programmatic first, then "name from content", same two-step resolution as `link-name-present`, same selector too). Links with no name at all are `link-name-present`'s concern, not this rule's.
1243
+
1244
+ **Expectation.** The link's full accessible name, normalized (trimmed, case-folded, trailing punctuation stripped), is not an exact match for a known non-descriptive phrase ("click here", "read more", "more", "here", "details", "link", etc., WCAG technique F84's known failure pattern for SC 2.4.4) or a bare file-format/type name ("HTML", "PDF", "EPUB", ...) with no adjacent context (an aria-describedby target, the enclosing list item/table cell/paragraph's own text, or (format names only) a table's first-row header) naming what it belongs to.
1245
+
1246
+ ### `list-children-valid`
1247
+
1248
+ **Lists must only directly contain list items**
1249
+
1250
+ automatic · WCAG 1.3.1 (A) · confidence high · default severity serious
1251
+
1252
+ Checks that <ul>/<ol> elements only have <li>, <script>, or <template> as direct children.
1253
+
1254
+ **Applies to.** Applies to <ul>/<ol> elements that have at least one direct element child.
1255
+
1256
+ **Expectation.** Every direct element child is <li>, <script>, or <template>. UNLESS it has an explicit `role` attribute, in which case the explicit role wins over the tag entirely: a child is valid iff that role is "listitem" (so `<li role="presentation">`/`<li role="menuitem">` are invalid despite the <li> tag, and conversely a non-<li> element explicitly given `role="listitem"` is valid). A wrapper <div> used for styling (no role at all) still breaks list semantics the same as before.
1257
+
1258
+ ### `listbox-name-present`
1259
+
1260
+ **Listboxes have an accessible name**
1261
+
1262
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1263
+
1264
+ Checks that elements with role="listbox" expose a non-empty accessible name.
1265
+
1266
+ **Applies to.** Applies to elements carrying role="listbox" (the attribute must name that role alone, not a fallback list) that are included in the accessibility tree. An element with the matching implicit role but no role attribute is out of scope.
1267
+
1268
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, or from title. role="listbox" is name-from-author-only, so subtree text is never accepted: text sitting inside a custom listbox widget is not reliably exposed as its name. On a labelable element (<select multiple role="listbox">) an associated <label> counts as well.
1269
+
1270
+ ### `listitem-parent-valid`
1271
+
1272
+ **List items must be inside a list container**
1273
+
1274
+ automatic · WCAG 1.3.1 (A) · confidence high · default severity serious
1275
+
1276
+ Checks that <li> elements are contained by <ul>, <ol>, or an element with role="list".
1277
+
1278
+ **Applies to.** Applies to <li> elements that have a parent element.
1279
+
1280
+ **Expectation.** The parent is <ul>/<ol> with no role override, or an element with an explicit role of "list", "presentation", or "none". An <li> used outside a real list container (e.g. as a generic flex/grid item under a <div>) is not exposed as a list item to assistive technologies.
1281
+
1282
+ ### `manual-review`
1283
+
1284
+ **Manual review: keyboard navigation and focus order**
1285
+
1286
+ manual · WCAG 2.1.1, 2.4.3, 2.4.7 (AA) · confidence medium · default severity moderate
1287
+
1288
+ Flags that a manual review of keyboard navigation and focus order is required.
1289
+
1290
+ **Applies to.** Applies to every run, whatever the page contains. Keyboard operability and focus order are properties of the page as a whole, and no markup pattern rules the question out.
1291
+
1292
+ **Expectation.** Always cantTell, carrying one occurrence at the scan root. Whether focus can leave every component, whether the tab order follows the reading order, and whether the focus indicator stays visible in use all need a person driving the page. The rule exists so that need is stated in the results rather than left for the reader to remember.
1293
+
1294
+ ### `media-alternative-transcript-evidence`
1295
+
1296
+ **Time-based media: transcript or text alternative evidence**
1297
+
1298
+ manual · WCAG 1.2.1 (A) · confidence low · default severity moderate
1299
+
1300
+ Finds audio and video elements where a transcript or other text alternative is not strongly evidenced in the page content. This rule is conservative and reports cantTell when evidence is missing or cannot be verified.
1301
+
1302
+ **Applies to.** Any eligible <audio> or <video> element in the composed DOM.
1303
+
1304
+ **Expectation.** If a strong transcript/text-alternative signal is present (e.g., aria-describedby binding to a visible transcript block, or a nearby clearly labeled Transcript section/link), no occurrence is reported. Otherwise, the rule reports cantTell (insufficient evidence) for that media element.
1305
+
1306
+ ### `menuitem-name-present`
1307
+
1308
+ **Menu items have an accessible name**
1309
+
1310
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1311
+
1312
+ Checks that menu items (role="menuitem*", including checkbox/radio variants) expose a non-empty accessible name.
1313
+
1314
+ **Applies to.** Applies to elements carrying role="menuitem", role="menuitemcheckbox" or role="menuitemradio" (the attribute must name one of those roles alone, not a fallback list) that are included in the accessibility tree.
1315
+
1316
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, from title, or, all three roles being name-from-content, from its own subtree text, where a descendant's own name (an <img alt>, aria-label or title) counts as that descendant's contribution rather than only its text nodes.
1317
+
1318
+ ### `meta-refresh-no-exceptions`
1319
+
1320
+ **Page must not use a meta refresh at all (AAA)**
1321
+
1322
+ automatic · WCAG 2.2.4, 3.2.5 (AAA) · confidence high · default severity moderate
1323
+
1324
+ Checks that <meta http-equiv="refresh"> is not present at all, regardless of delay. This is the stricter AAA-level counterpart of the A-level positive-delay-only check.
1325
+
1326
+ **Applies to.** Applies to the first <meta http-equiv="refresh"> element, in document order, with a valid content attribute, per HTML's shared declarative refresh steps, a document only ever acts on its first valid meta refresh, so a later one (valid or not) is inert and out of scope.
1327
+
1328
+ **Expectation.** Running the shared declarative refresh steps against that element's content value results in a delay of exactly 0. An immediate (delay=0) redirect still passes at AAA, same as the A-level rule. There is nothing for a user to be interrupted mid-read by when nothing is displayed first. Any positive delay fails, with none of the A-level rule's >20-hour exemption: at AAA, WCAG 2.2.4 (Interruptions) and 3.2.5 (Change on Request) require that a *timed* automatic context change happen only at the user's request, regardless of how long the timer is.
1329
+
1330
+ ### `meta-refresh-timing-absent`
1331
+
1332
+ **Page must not use a timed meta refresh**
1333
+
1334
+ automatic · WCAG 2.2.1 (A) · confidence high · default severity serious
1335
+
1336
+ Checks that <meta http-equiv="refresh"> does not impose a positive delay of 20 hours or less.
1337
+
1338
+ **Applies to.** Applies to the first <meta http-equiv="refresh"> element, in document order, whose content attribute has a parseable leading delay value. Per HTML's shared declarative refresh steps, a document only ever acts on its first valid meta refresh; any later one (valid or not) is inert markup a browser never processes, so it is not evaluated.
1339
+
1340
+ **Expectation.** The delay is 0 (an immediate redirect, which users cannot be caught by mid-read), or exceeds 20 hours. Any other positive delay refreshes or redirects the page on a timer the user did not initiate and cannot pause, stop, or extend, which WCAG 2.2.1 (Timing Adjustable) requires be possible.
1341
+
1342
+ ### `meta-viewport-large`
1343
+
1344
+ **Viewport meta tag should allow zooming up to 500%**
1345
+
1346
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1347
+
1348
+ Checks that <meta name="viewport"> does not set user-scalable=no or maximum-scale below 5 (500%).
1349
+
1350
+ **Applies to.** Applies to <meta name="viewport"> elements that carry a non-empty content attribute.
1351
+
1352
+ **Expectation.** The content attribute does not set user-scalable to "no"/"0", and does not set maximum-scale below 5 (500%). This is the AAA-level, stricter counterpart of meta-viewport-zoom-enabled (which enforces the AA 200% minimum as a hard, WCAG-normative fail); this rule is advisory best-practice guidance toward the higher AAA bar.
1353
+
1354
+ ### `meta-viewport-zoom-enabled`
1355
+
1356
+ **Viewport meta tag must not disable zoom**
1357
+
1358
+ automatic · WCAG 1.4.4 (AA) · confidence high · default severity serious
1359
+
1360
+ Checks that <meta name="viewport"> does not set user-scalable=no or maximum-scale below 2 (200%).
1361
+
1362
+ **Applies to.** Applies to <meta name="viewport"> elements whose content attribute sets maximum-scale or user-scalable. Content setting neither cannot restrict zoom.
1363
+
1364
+ **Expectation.** user-scalable is absent, yes, device-width, device-height, or a number outside the range -1 to 1; and maximum-scale is absent, device-width, device-height, negative, or 2 or more. Anything else stops the user zooming text to 200%, which WCAG 1.4.4 (Resize Text) requires.
1365
+
1366
+ ### `meter-name-present`
1367
+
1368
+ **Meters have an accessible name**
1369
+
1370
+ automatic · WCAG 1.1.1 (A) · confidence high · default severity serious
1371
+
1372
+ Checks that elements with role="meter" expose a non-empty accessible name.
1373
+
1374
+ **Applies to.** Applies to elements carrying role="meter" (the attribute must name that role alone, not a fallback list) that are included in the accessibility tree. An element with the matching implicit role but no role attribute is out of scope.
1375
+
1376
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, or from title. role="meter" is name-from-author-only, so subtree text is never accepted: text sitting inside a custom meter widget is not reliably exposed as its name.
1377
+
1378
+ ### `mouse-only-event-handlers`
1379
+
1380
+ **Pointer-only inline event handlers should have a keyboard-reachable equivalent**
1381
+
1382
+ manual · WCAG 2.1.1 (A) · confidence low · default severity moderate
1383
+
1384
+ Flags elements with an inline pointer-only event handler (onmouseover, onmouseout, onmousedown, onmouseup, ondblclick, onmousemove, onmouseenter, onmouseleave) and no keyboard-reachable equivalent (onkeydown/onkeyup/onkeypress/onfocus/onblur), for manual review.
1385
+
1386
+ **Applies to.** Elements carrying at least one inline pointer-only event-handler attribute (`onmouseover`, `onmouseout`, `onmousedown`, `onmouseup`, `ondblclick`, `onmousemove`, `onmouseenter`, `onmouseleave`) with a non-empty value, that are also eligible/reachable (not hidden/`aria-hidden`/`display:none`).
1387
+
1388
+ **Expectation.** The element also carries at least one keyboard-reachable inline handler: `onkeydown`, `onkeyup`, `onkeypress` (the direct keyboard- event equivalents), or `onfocus`/`onblur` (the standard substitute for hover-triggered behavior: focus/blur are the keyboard- navigable analog to mouseover/mouseout, per WCAG technique G90). Otherwise the element's mouse-driven behavior (a hover tooltip, a custom dropdown, a drag interaction) has no way to be triggered by a keyboard-only user.
1389
+
1390
+ ### `nested-interactive-controls-absent`
1391
+
1392
+ **Interactive controls must not be nested**
1393
+
1394
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1395
+
1396
+ Checks that an interactive control (link, button, form control, or ARIA widget role) does not contain another interactive control.
1397
+
1398
+ **Applies to.** Applies to elements matching the interactive-control set (native a[href], button, input (not hidden), select, textarea; or an explicit ARIA widget role: button, link, checkbox, radio, switch, tab, textbox, combobox, listbox, menuitem, menuitemcheckbox, menuitemradio, option, slider, spinbutton, searchbox, treeitem). The container is applicable regardless of whether it is itself focusable, focusability is only used to decide whether a *descendant* nests an interactive control.
1399
+
1400
+ **Expectation.** The element does not contain, as a descendant, another *operable* interactive control (e.g. a <button> wrapping a <select>, or a link containing a checkbox). Nested interactive controls are not reliably announced or operable via assistive technology, activating the outer control and the inner one become ambiguous, and some AT only exposes one of the two.
1401
+
1402
+ ### `no-autoplay-audio`
1403
+
1404
+ **Autoplaying audio should provide a pause/stop or volume-control mechanism**
1405
+
1406
+ manual · WCAG 1.4.2 (A) · confidence low · default severity moderate
1407
+
1408
+ Flags <audio>/<video> elements that autoplay unmuted with no native controls attribute, for manual review against the 3-second exemption in WCAG 1.4.2.
1409
+
1410
+ **Applies to.** Any <audio autoplay> or <video autoplay> element that is not `muted`.
1411
+
1412
+ **Expectation.** SC 1.4.2 only applies when audio plays automatically for MORE than 3 seconds; clip duration is not knowable from static markup (jsdom does not decode media), so this rule cannot determine whether the SC even applies to a given element. It is authored as `type: 'manual'` (cantTell-capped, never fail) on purpose rather than guessing: an autoplaying unmuted element with no `controls` attribute (the native, statically-verifiable mechanism to pause/stop or adjust volume) is flagged for human review rather than treated as a deterministic violation.
1413
+
1414
+ ### `object-text-alternative-present`
1415
+
1416
+ **<object> must provide a text alternative**
1417
+
1418
+ automatic · WCAG 1.1.1 (A) · confidence high · default severity serious
1419
+
1420
+ Checks that <object> elements provide a text alternative via fallback content or an accessible name.
1421
+
1422
+ **Applies to.** Applies to <object> elements included in the accessibility tree. Hidden objects are excluded whether or not they are focusable. role="presentation"/role="none" are excluded only when not focusable.
1423
+
1424
+ **Expectation.**
1425
+
1426
+ Each applicable <object> provides a text alternative via:
1427
+
1428
+ - fallback content (non-empty text content inside <object>), OR
1429
+ - an accessible name (aria-labelledby/aria-label), OR
1430
+ - a title attribute (best-effort fallback).
1431
+
1432
+ ### `object-text-alternative-quality`
1433
+
1434
+ **<object> text alternative must be appropriate (manual review)**
1435
+
1436
+ manual · WCAG 1.1.1 (A) · confidence medium · default severity minor
1437
+
1438
+ Flags <object> elements with detected fallback or name for human review of equivalence and appropriateness.
1439
+
1440
+ **Applies to.** Applies to <object> elements that already carry a text alternative: fallback text content, a non-empty aria-label, an aria-labelledby that resolves to non-empty text, or a non-empty title. An <object> with none of those is object-text-alternative-present's failure, not a quality question. The element must be included in the accessibility tree, and role="presentation"/"none" takes it out of scope unless it is focusable, which restores its role.
1441
+
1442
+ **Expectation.** Human review is required to confirm that the provided text alternative is accurate and appropriate.
1443
+
1444
+ ### `option-name-present`
1445
+
1446
+ **Options have an accessible name**
1447
+
1448
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1449
+
1450
+ Checks that elements with role="option" expose a non-empty accessible name.
1451
+
1452
+ **Applies to.** Applies to elements carrying role="option" (the attribute must name that role alone, not a fallback list) that are included in the accessibility tree. An element with the matching implicit role but no role attribute is out of scope.
1453
+
1454
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, from title, or, role="option" being name-from-content, from its own subtree text, where a descendant's own name (an <img alt>, aria-label or title) counts as that descendant's contribution rather than only its text nodes.
1455
+
1456
+ ### `p-as-heading`
1457
+
1458
+ **A <p> styled to look like a heading should probably be a real heading**
1459
+
1460
+ manual · WCAG 1.3.1 (A) · confidence low · default severity minor
1461
+
1462
+ Flags short <p> elements whose entire text is bold and rendered at >=18px, for manual review of whether a real heading element should be used instead.
1463
+
1464
+ **Applies to.** `<p>` elements with short (<=120 char), non-empty trimmed text content that is entirely bold (the `<p>`'s own computed `font-weight` >= 700, OR its entire text is wrapped in a single `<strong>`/`<b>` child) and rendered at >=18px.
1465
+
1466
+ **Expectation.** Text styled to visually read as a heading (bold, larger-than-body size, short) should be marked up with a real heading element (`<h1>`-`<h6>` or `role="heading"`) so its structural role is programmatically determinable, the same 1.3.1 concern as any other "structure conveyed through presentation only" issue.
1467
+
1468
+ ### `page-has-heading-one`
1469
+
1470
+ **Page should have a level-one heading**
1471
+
1472
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1473
+
1474
+ Checks that the page has at least one level-one heading (<h1> or role="heading" with aria-level="1").
1475
+
1476
+ **Applies to.** Always applicable to any HTML document with a <body> element: "does the page have an h1" is a whole-page concern, matching bypass-blocks-present's pattern of evaluating the document directly.
1477
+
1478
+ **Expectation.** At least one heading with level 1 exists (native <h1>, or role="heading" with aria-level="1"). A page with no top-level heading has no clear entry point for assistive technology users navigating by heading.
1479
+
1480
+ ### `page-title-patterns`
1481
+
1482
+ **Page title patterns that may be insufficiently descriptive**
1483
+
1484
+ manual · WCAG 2.4.2 (A) · confidence medium · default severity minor
1485
+
1486
+ Identifies page title patterns that may indicate low descriptiveness, such as generic, duplicated, or overly templated titles. This rule provides review signals and does not fail automatically.
1487
+
1488
+ **Applies to.** Applies to a run over a whole document whose <title> resolves to non-empty text; a missing or empty title is page-title-present's failure, not a pattern to review. A run narrowed by contextSelector or by engineOptions.fragment is notApplicable, as is a title matching none of the patterns below.
1489
+
1490
+ **Expectation.** The title carries none of the conservative low-descriptiveness signals: one of the generic titles home, homepage, welcome, untitled, page or document; fewer than eight characters; or a template shape pairing a generic token with a brand, such as "Home | Brand". When the crawl.pageTitles probe supplies at least ten pages, cross-page signals are used instead: one title repeated across distinct URLs, or a prefix or suffix of twelve characters or more shared across the set. Every signal is reported as cantTell: whether a title describes its page is a judgment, so the rule never fails on a pattern alone.
1491
+
1492
+ ### `page-title-present`
1493
+
1494
+ **Page has a non-empty title**
1495
+
1496
+ automatic · WCAG 2.4.2 (A) · confidence high · default severity serious
1497
+
1498
+ Checks that the page includes a non-empty <title> element.
1499
+
1500
+ **Applies to.** Applies to a run over a whole document. A run narrowed by contextSelector, or by engineOptions.fragment, is notApplicable: whether the page has a title is not a property any subtree can answer.
1501
+
1502
+ **Expectation.** The document has a <title> element, and document.title with whitespace collapsed is non-empty. The element is looked for anywhere in the document, not only inside <head>: a <title> the parser leaves outside <head> is still the document title in every browser. Whether that title describes the page is page-title-patterns' question.
1503
+
1504
+ ### `password-paste-enabled`
1505
+
1506
+ **Authentication fields must not block pasting**
1507
+
1508
+ manual · WCAG 3.3.8 (AA) · confidence medium · default severity serious
1509
+
1510
+ Checks that a password or one-time-code field carries no inline paste handler that cancels the paste, which would remove the password manager or clipboard that WCAG 3.3.8 relies on as the assisting mechanism.
1511
+
1512
+ **Applies to.** Applies to any control whose autocomplete token is current-password, new-password or one-time-code, and to <input type="password"> unless its autocomplete names another purpose. A disabled or readonly field takes no input to block, and one outside the accessibility tree is not being asked for, so neither is in scope.
1513
+
1514
+ **Expectation.** A reviewer confirms the field can still be pasted into. Remembering a password is a cognitive function test, and 3.3.8 asks for a mechanism that helps the user through one; a password manager, or the clipboard for a one-time code, is that mechanism.
1515
+
1516
+ ### `presentation-role-conflict`
1517
+
1518
+ **Presentational role must not conflict with a global ARIA attribute or focusability**
1519
+
1520
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1521
+
1522
+ Checks that role="presentation"/"none" (including an <img alt=""> implicit presentation role) is not combined with a global ARIA attribute (aria-label, aria-hidden, aria-describedby, ...) or focusability (tabindex/native).
1523
+
1524
+ **Applies to.** Applies to elements with an explicit role="presentation" or role="none", OR an <img alt=""> carrying no explicit role of its own (empty alt gives an <img> an implicit presentation role per HTML-AAM, even with no explicit role attribute at all: `img[alt=''], [role="none"], [role="presentation"]`).
1525
+
1526
+ **Expectation.** The element does not also carry a WAI-ARIA *global* state/property (aria-label, aria-hidden, aria-describedby, aria-live, aria-current, ...; the full global-attribute set, not just the naming ones), AND is not focusable. Per the WAI-ARIA spec's Presentational Roles Conflict Resolution section, a presentational role is "restored" to the element's implicit semantic role when either condition holds: the presentation/none role silently stops working, contradicting the author's evident intent to hide the element from the accessibility tree.
1527
+
1528
+ ### `presentational-children-focusable-absent`
1529
+
1530
+ **Roles with presentational children must not contain focusable content**
1531
+
1532
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1533
+
1534
+ Checks that an element whose role makes its children presentational (button, checkbox, img, option, radio, slider, switch, tab, ...) contains no descendant that takes a tab stop.
1535
+
1536
+ **Applies to.** Applies to elements whose semantic role is one of the WAI-ARIA roles defined as having presentational children (button, checkbox, img, menuitemcheckbox, menuitemradio, meter, option, progressbar, radio, scrollbar, separator, slider, switch, tab, plus doc-pagebreak and graphics-symbol from the DPUB-ARIA/Graphics-ARIA modules, which inherit the same trait). The role can be explicit (role="tab") or native (<button>, <meter>, <progress>, <option>).
1537
+
1538
+ **Expectation.** No descendant of the element is part of sequential focus navigation. The presentational-children mechanism removes every descendant from the accessibility tree, so a descendant that still takes a tab stop receives focus with no role and no name to announce.
1539
+
1540
+ ### `progressbar-name-present`
1541
+
1542
+ **Progress bars have an accessible name**
1543
+
1544
+ automatic · WCAG 1.1.1 (A) · confidence high · default severity serious
1545
+
1546
+ Checks that elements with role="progressbar" expose a non-empty accessible name.
1547
+
1548
+ **Applies to.** Applies to elements carrying role="progressbar" (the attribute must name that role alone, not a fallback list) that are included in the accessibility tree. An element with the matching implicit role but no role attribute is out of scope.
1549
+
1550
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, or from title. role="progressbar" is name-from-author-only, so subtree text is never accepted: text sitting inside a custom progressbar widget is not reliably exposed as its name.
1551
+
1552
+ ### `region`
1553
+
1554
+ **Page content should be inside a landmark region**
1555
+
1556
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1557
+
1558
+ Checks that content under <body> is contained within a landmark region.
1559
+
1560
+ **Applies to.** Applies to any element under <body> that directly carries visible text (or other own content, see @implementation-notes) and is not itself a landmark, live region, dialog, button, <svg>, <iframe>/<frame>, or a resolvable skip-link.
1561
+
1562
+ **Expectation.** Every top-level piece of page content lives inside a landmark region (main, navigation, banner, contentinfo, complementary, region, form, search), so assistive technology users navigating by landmark do not miss content that was never placed inside one.
1563
+
1564
+ ### `role-img-text-alternative-present`
1565
+
1566
+ **[role="img"/"graphics-symbol"/"graphics-document"] must have an accessible text alternative**
1567
+
1568
+ automatic · WCAG 1.1.1 (A) · confidence high · default severity serious
1569
+
1570
+ Checks that elements with role="img", "graphics-symbol" or "graphics-document" provide an accessible text alternative using aria-label, aria-labelledby, a title attribute, or (for SVG elements) a first-child <title>.
1571
+
1572
+ **Applies to.** Applies to elements with role="img", role="graphics-symbol" or role="graphics-document" that are included in the accessibility tree (ACT 23a2a8's "programmatically hidden" exemption: display:none/visibility:hidden/aria-hidden="true" on the element or an ancestor, with no carve-out for focusable or IDREF-referenced elements, aria-hidden-focus and duplicate-id-aria own those separately). Per ACT 7d6734, this reaches any element carrying one of these roles, not only the <svg> root, e.g. a <circle role="graphics-symbol"> nested inside a plain <svg>. The <svg> root itself is left to svg-text-alternative-present's own (already ACT-clean) title/aria-name handling; the <img> tag is excluded here since it has its own dedicated rule.
1573
+
1574
+ **Expectation.**
1575
+
1576
+ Each applicable element has an accessible text alternative:
1577
+
1578
+ - aria-label with a non-empty value; OR
1579
+ - aria-labelledby referencing at least one existing element that contributes non-empty text; OR
1580
+ - a non-empty title attribute (last-resort accessible-name source per HTML-AAM); OR
1581
+ - for an SVG-namespace element, a non-empty first-child <title> (SVG-AAM's own naming mechanism, not only for the <svg> root).
1582
+
1583
+ ### `scope-attr-valid`
1584
+
1585
+ **scope attribute must have a valid value**
1586
+
1587
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1588
+
1589
+ Checks that scope="..." is one of row, col, rowgroup, or colgroup.
1590
+
1591
+ **Applies to.** Applies to elements with a non-empty scope attribute.
1592
+
1593
+ **Expectation.** The scope value is one of "row", "col", "rowgroup", or "colgroup" (case-insensitive). An invalid scope value is not recognized by assistive technology, silently losing the row/column header association it was meant to declare.
1594
+
1595
+ ### `scrollable-region-focusable`
1596
+
1597
+ **Scrollable regions with no focusable content should be keyboard-focusable**
1598
+
1599
+ manual · WCAG 2.1.1, 2.1.3 (AAA) · confidence low · default severity moderate
1600
+
1601
+ Flags elements whose CSS declares overflow:auto/scroll, contain no focusable descendant, and are not themselves keyboard-focusable, for manual review of whether their content actually overflows and needs keyboard scroll access.
1602
+
1603
+ **Applies to.** Scoped on purpose to a fixed set of likely-to-scroll container tags (div, section, article, aside, main, nav, pre, table, blockquote, ul, ol, textarea) with computed `overflow-x`/`overflow-y` of `auto` or `scroll`, not every element on the page, to keep this deterministic and performant (same style of scope-down as `region`).
1604
+
1605
+ **Expectation.** A region whose CSS declares it may scroll (`auto`/`scroll`) should be reachable by keyboard: either it already contains a focusable descendant (a link, button, form control, or `tabindex`-bearing element a keyboard user could tab into and then use arrow keys to scroll from), or the region itself carries a non-negative `tabindex`.
1606
+
1607
+ ### `searchbox-name-present`
1608
+
1609
+ **Searchboxes have an accessible name**
1610
+
1611
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1612
+
1613
+ Checks that elements with role="searchbox" expose a non-empty accessible name.
1614
+
1615
+ **Applies to.** Applies to elements carrying role="searchbox" (the attribute must name that role alone, not a fallback list) that are included in the accessibility tree. An element with the matching implicit role but no role attribute is out of scope.
1616
+
1617
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, or from title. role="searchbox" is name-from-author-only, so subtree text is never accepted: text sitting inside a custom searchbox widget is not reliably exposed as its name. On a labelable element (<input role="searchbox">) an associated <label> counts as well.
1618
+
1619
+ ### `server-side-image-map-absent`
1620
+
1621
+ **Images must not use a server-side image map**
1622
+
1623
+ automatic · WCAG 2.1.1 (A) · confidence high · default severity serious
1624
+
1625
+ Checks that <img> elements do not carry the ismap attribute (server-side image maps have no keyboard-operable equivalent).
1626
+
1627
+ **Applies to.** Applies to <img> elements that carry an ismap attribute.
1628
+
1629
+ **Expectation.** The image does not use ismap at all. Server-side image maps depend on the browser sending click coordinates to the server, which has no keyboard-operable equivalent, there is no way to determine or expose individual clickable regions to assistive technology or keyboard users. Client-side image maps (<map>/<area>, each with real href/alt) are the accessible alternative and are not flagged by this rule.
1630
+
1631
+ ### `skip-link`
1632
+
1633
+ **Skip link must have a resolvable, usable target**
1634
+
1635
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1636
+
1637
+ Checks that a "skip to ..." link's href fragment resolves to a real, currently usable element in the document.
1638
+
1639
+ **Applies to.** Applies to <a href="#fragment"> elements whose accessible name matches a common "skip to ..." / "jump to ..." authoring convention (case-insensitive "skip" or "jump to" in the name), the recognizable pattern for a skip-navigation link, not every same-page anchor link on the page. "jump to" is included alongside "skip" since real skip links use both conventions (e.g. a "Jump to section" link, which a purely positional match would catch but a "skip"-only text pattern would miss). Text-pattern matching itself stays intentional (see implementation-notes); this only widens the known-convention list.
1640
+
1641
+ **Expectation.** The link's fragment resolves to a real element in the document (via a matching id, or a legacy <a name="...">), and that target is currently usable (not hidden from the accessibility tree; and, when browser geometry is available, not zero-area/no-rects). A skip link whose target is missing or effectively unusable does not provide a reliable bypass destination.
1642
+
1643
+ ### `slider-name-present`
1644
+
1645
+ **Sliders have an accessible name**
1646
+
1647
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1648
+
1649
+ Checks that sliders (input[type="range"] and role="slider") expose a non-empty accessible name.
1650
+
1651
+ **Applies to.** Applies to elements carrying role="slider" (the attribute must name that role alone, not a fallback list) that are included in the accessibility tree. An element with the matching implicit role but no role attribute is out of scope. A native <input type="range"> without the role belongs to form-control-programmatic-label-present.
1652
+
1653
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, or from title. role="slider" is name-from-author-only, so subtree text is never accepted: text sitting inside a custom slider widget is not reliably exposed as its name. On a labelable element (<input role="slider">) an associated <label> counts as well.
1654
+
1655
+ ### `spinbutton-name-present`
1656
+
1657
+ **Spinbuttons have an accessible name**
1658
+
1659
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1660
+
1661
+ Checks that elements with role="spinbutton" expose a non-empty accessible name.
1662
+
1663
+ **Applies to.** Applies to elements carrying role="spinbutton" (the attribute must name that role alone, not a fallback list) that are included in the accessibility tree. An element with the matching implicit role but no role attribute is out of scope.
1664
+
1665
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, or from title. role="spinbutton" is name-from-author-only, so subtree text is never accepted: text sitting inside a custom spinbutton widget is not reliably exposed as its name. On a labelable element (<input role="spinbutton">) an associated <label> counts as well.
1666
+
1667
+ ### `summary-name-present`
1668
+
1669
+ **Summary elements have an accessible name**
1670
+
1671
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1672
+
1673
+ Checks that <summary> elements expose a non-empty accessible name.
1674
+
1675
+ **Applies to.** Applies to <summary> elements included in the accessibility tree, wherever they appear, a <summary> outside a <details> is still matched.
1676
+
1677
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, from title, or from its own subtree text, where a descendant's own name (an <img alt>, aria-label or title) counts as that descendant's contribution.
1678
+
1679
+ ### `svg-image-text-alternative-present`
1680
+
1681
+ **SVG <image> must have a text alternative**
1682
+
1683
+ automatic · WCAG 1.1.1 (A) · confidence medium · default severity serious
1684
+
1685
+ Checks that SVG <image> elements provide a text alternative via <title>/<desc> or an ARIA accessible name.
1686
+
1687
+ **Applies to.** Applies to SVG <image> elements that are exposed to assistive technologies. Elements otherwise hidden from the accessibility tree remain applicable if they are tabbable or referenced by IDREF relationships (per engine eligibility checks). SVG <image> elements with role="presentation" or role="none" are excluded only when they are not focusable.
1688
+
1689
+ **Expectation.**
1690
+
1691
+ Each applicable SVG <image> element has a text alternative via:
1692
+
1693
+ - a non-empty direct <title> child, OR
1694
+ - a non-empty direct <desc> child, OR
1695
+ - an accessible name (aria-label / aria-labelledby / title attribute).
1696
+
1697
+ ### `svg-text-alternative-present`
1698
+
1699
+ **<svg> must provide a text alternative**
1700
+
1701
+ automatic · WCAG 1.1.1 (A) · confidence high · default severity serious
1702
+
1703
+ Checks that inline <svg> elements provide a text alternative via a <title> element or an ARIA name (a <desc> element alone does not count).
1704
+
1705
+ **Applies to.**
1706
+
1707
+ Applies to inline <svg> elements that are exposed to assistive technologies AND appear intended to be conveyed. "Intended to be conveyed" is approximated deterministically by at least one of:
1708
+
1709
+ - role="img", role="graphics-symbol", or role="graphics-document" on the SVG root element itself (the WAI-ARIA Graphics Module roles, alongside img). Does NOT extend to arbitrary role="img"/"graphics-symbol"/"graphics-document" descendants nested inside an <svg>. This check's scope is the <svg> root only; role-img-text-alternative-present covers those same three roles on any other element, including nested SVG shapes (ACT 7d6734's own failed example: a bare `<svg>` root with a nested `<circle role="graphics-symbol">`).
1710
+ - aria-label / aria-labelledby present
1711
+ - <title> or <desc> present (desc alone is an applicability signal only, see @expectation)
1712
+ - focusable/tabbable (e.g., tabindex, native focusability)
1713
+
1714
+ Images with role="presentation" or role="none" are excluded only when they are not focusable. Elements otherwise hidden from the accessibility tree remain applicable if they are tabbable-focusable or referenced by IDREF relationships (per engine eligibility checks).
1715
+
1716
+ **Expectation.**
1717
+
1718
+ Each applicable <svg> element provides a text alternative via:
1719
+
1720
+ - non-empty <title> text, OR
1721
+ - an ARIA name (aria-label / aria-labelledby).
1722
+
1723
+ A <desc> element alone does NOT satisfy this, per the SVG Accessibility API Mappings spec §7.1, <desc> only ever contributes to the accessible DESCRIPTION, never the accessible NAME. An <svg> with only a <desc> and no <title>/ARIA name is still "applicable" (desc signals authorial intent) but fails.
1724
+
1725
+ ### `svg-text-alternative-quality`
1726
+
1727
+ **<svg> text alternative must be appropriate (manual review)**
1728
+
1729
+ manual · WCAG 1.1.1 (A) · confidence medium · default severity minor
1730
+
1731
+ Flags applicable <svg> graphics with a detected text alternative for human review of appropriateness.
1732
+
1733
+ **Applies to.** Applies to inline <svg> elements that already carry a text alternative: non-empty <title> or <desc> text, a non-empty aria-label, or an aria-labelledby that resolves to non-empty text. <desc> counts here as something to review even though it never contributes to the accessible name: that distinction is svg-text-alternative-present's. The element must be included in the accessibility tree, and role="presentation"/"none" takes it out of scope unless it is focusable, which restores its role.
1734
+
1735
+ **Expectation.** Human review is required to confirm that the provided text alternative is accurate and appropriate.
1736
+
1737
+ ### `tab-name-present`
1738
+
1739
+ **Tabs have an accessible name**
1740
+
1741
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1742
+
1743
+ Checks that elements with role="tab" expose a non-empty accessible name.
1744
+
1745
+ **Applies to.** Applies to elements carrying role="tab" (the attribute must name that role alone, not a fallback list) that are included in the accessibility tree. An element with the matching implicit role but no role attribute is out of scope.
1746
+
1747
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, from title, or, role="tab" being name-from-content, from its own subtree text, where a descendant's own name (an <img alt>, aria-label or title) counts as that descendant's contribution rather than only its text nodes.
1748
+
1749
+ ### `tabindex`
1750
+
1751
+ **tabindex should not be greater than 0**
1752
+
1753
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1754
+
1755
+ Checks that tabindex values are 0 or negative, not a positive number.
1756
+
1757
+ **Applies to.** Applies to elements with a tabindex attribute whose value parses as a valid integer.
1758
+
1759
+ **Expectation.** The tabindex value is 0 or negative. A positive tabindex reorders keyboard tab order explicitly, which is fragile to maintain as a page changes and usually indicates the natural DOM order should be fixed instead.
1760
+
1761
+ ### `table-duplicate-name`
1762
+
1763
+ **Table caption must not duplicate its summary attribute**
1764
+
1765
+ manual · no formal WCAG SC mapping · confidence medium · default severity minor
1766
+
1767
+ Checks that a <table>'s <caption> text is not identical to its (deprecated) summary attribute.
1768
+
1769
+ **Applies to.** Applies to <table> elements that have both a <caption> with text content and a (deprecated but still encountered) summary attribute.
1770
+
1771
+ **Expectation.** The caption text and the summary attribute text are not identical (case-insensitive, normalized). When both are present and say the same thing, assistive technology that surfaces both announces the same text twice for one table.
1772
+
1773
+ ### `table-fake-caption`
1774
+
1775
+ **A table's first row should not stand in for a real <caption>**
1776
+
1777
+ manual · WCAG 1.3.1 (A) · confidence low · default severity minor
1778
+
1779
+ Flags tables with no <caption> whose first row has a single non-empty cell while other rows have multiple cells, for manual review of whether that cell is acting as a fake caption.
1780
+
1781
+ **Applies to.** `<table>` elements with no `<caption>` child, at least two rows, and a first row containing exactly one non-empty-text cell while at least one other row has more than one cell.
1782
+
1783
+ **Expectation.** A single lone cell in the first row, sitting above rows that clearly have multiple columns, strongly suggests the author is using it as a visual caption/title rather than as a real table cell. Structure conveyed only through this positional convention is not programmatically associated with the table the way a real `<caption>` element is (1.3.1).
1784
+
1785
+ ### `table-headers-attr-valid`
1786
+
1787
+ **Table cell "headers" attribute must reference valid header cells**
1788
+
1789
+ automatic · WCAG 1.3.1 (A) · confidence high · default severity serious
1790
+
1791
+ Checks that each id in a <td>/<th> headers attribute resolves to a cell (<td> or <th>) within the same table (not missing, not a non-cell element, not itself).
1792
+
1793
+ **Applies to.** Applies to <td>/<th> elements that carry a non-empty headers attribute, within a <table> whose semantic role is still table/grid/treegrid -- an explicit role of anything else (role="presentation"/"none", but also role="heading" or any other real role) replaces the native table semantics, leaving no table for headers to describe. Matches ACT a25f45's applicability.
1794
+
1795
+ **Expectation.** Every id token in the headers attribute resolves to an element that: (a) exists, (b) is a cell (<td> or <th>) of the same <table> as the referencing cell, and (c) is not the cell itself. A <td> serving as a header via role="columnheader"/"rowheader" is a valid target, same as a plain <th> -- ACT a25f45 does not require the native tag.
1796
+
1797
+ ### `table-th-has-data-cells`
1798
+
1799
+ **<th> elements must describe at least one data cell**
1800
+
1801
+ automatic · WCAG 1.3.1 (A) · confidence high · default severity moderate
1802
+
1803
+ Checks that a table containing <th> elements also contains at least one <td> data cell for those headers to describe.
1804
+
1805
+ **Applies to.** Applies to <table> elements that keep their table semantics and are included in the accessibility tree, and that contain at least one <th> which is visible, included in the accessibility tree, and not overridden by an explicit role other than rowheader/columnheader. Also applies to the ARIA-only equivalent: an element with role="grid"/"treegrid" (no native <table> involved) that contains at least one in-scope columnheader/rowheader-role element.
1806
+
1807
+ **Expectation.** The table also contains at least one <td> somewhere in it.
1808
+
1809
+ ### `target-size-minimum`
1810
+
1811
+ **Pointer targets must be at least 24x24px large, or leave sufficient distance to other targets**
1812
+
1813
+ automatic · WCAG 2.5.8 (AA) · confidence medium · default severity serious
1814
+
1815
+ Checks that pointer-operable targets have an effective hit region of at least 24 by 24 CSS pixels, or meet an allowed exception (e.g. sufficient spacing).
1816
+
1817
+ **Applies to.** Applies to <button>, <summary>, <a href>, <area href>, <input>, <select>, <textarea> and elements with role="button"/"link" that are pointer-reachable: rendered, not suppressed by pointer-events:none, and with a measurable box of non-zero size. Accessibility-tree exclusion isn't a filter here: an aria-hidden control is still a target a pointer can hit. <area> is matched but never actually evaluated, for the reason given below.
1818
+
1819
+ **Expectation.**
1820
+
1821
+ Each target is at least 24 by 24 CSS pixels, or meets one of the SC 2.5.8 exceptions this rule can establish from geometry: spacing (a 24px-diameter circle centred on the target reaches no unrelated target), the inline exception for a link inside a run of text, or user-agent sizing (an unstyled native checkbox or radio, detected by appearance not having been reset to none). An undersized target too close to a neighbour fails. Where an exception may apply but geometry cannot confirm it (two inline links in one run of text, or a target inside an SVG, canvas or image map that may be essential), the result is cantTell rather than a guess. Notes (engine intent):
1822
+
1823
+ - This rule is DOM-based and measures pointer hit regions available to sighted pointer users.
1824
+ - Elements can be "pointer-operable" even if excluded from the accessibility tree (e.g. aria-hidden="true").
1825
+ - Excludes targets that are not pointer-reachable due to rendering suppression (display:none, etc.), or pointer suppression (pointer-events:none), or zero geometry (e.g. scale(0) -> zero rects).
1826
+
1827
+ WCAG 2.5.8 exceptions implemented, and how:
1828
+
1829
+ - Spacing: a 24px-diameter circle centered on an undersized target must not intersect another (unrelated) target's box or another undersized target's own circle. Two passes: a fast center-distance check (exact for undersized-vs-undersized, a reasonable proxy otherwise) and a 16-point perimeter sample via elementFromPoint as a more precise fallback for cases the distance check under-detects (e.g. a small target adjacent to a large, elongated neighbor). Ancestor/descendant relationships between the target and the "other" element are never treated as a conflict (see isRelated): a nested-interactive shape, a small control inside its own wrapping link/button, is one visual region, not two independent targets. That pattern is nested-interactive-controls-absent's concern, not a spacing one.
1830
+ - Inline: a link inside a text-block container passes outright (isInlineTextExceptionTarget). An inline link whose only spacing conflict is another inline link in the same run is reported as cantTell (isInlineLinkTarget), since the inline exception may cover it but geometry can't confirm that.
1831
+ - User Agent Control: an unstyled native checkbox/radio, detected via `appearance` not being reset to `none` (see isUserAgentSizedControl). Scoped narrowly to checkbox/radio specifically, not every form control, since those are the only types with unambiguous native rendering.
1832
+ - Essential/Equivalent: only a narrow, high-confidence subset is asserted (SVG/canvas/map-embedded controls, see isPlausiblyEssentialOrEquivalent); anything else defers to cantTell rather than guessing "essential" from a layout container.
1833
+
1834
+ Known gap, left unimplemented on purpose: `<area>` (image-map hotspot) elements are not evaluated at all. `area[href]` is in CANDIDATE_SELECTOR for forward-compatibility, but it's currently a no-op. `<area>` has no CSS box of its own (`display: none` by the HTML spec's default UA stylesheet, confirmed against the spec rather than a jsdom quirk), so `getBoundingClientRect()` always reports zero geometry and `isPointerReachable`'s existing `display:none` check rejects it before any size/exception logic runs. A real `<area>` hit-region is computed by the browser from its `shape`/`coords` attributes against the associated `<img>`'s *rendered* size, an entirely different measurement path than every other candidate here. Implementing that properly (parsing `coords`, resolving the owning `<img>` via its `usemap`, accounting for the image's CSS-scaled render size) is a separate, larger feature, not attempted in this pass. This is an automatic, deterministic approximation intended to be:
1835
+
1836
+ - strict on clear failures,
1837
+ - conservative when exceptions cannot be determined reliably.
1838
+
1839
+ ### `td-has-header`
1840
+
1841
+ **Data cells in large tables must have an associated header**
1842
+
1843
+ automatic · WCAG 1.3.1 (A) · confidence high · default severity serious
1844
+
1845
+ Checks that every <td> in a large, simple (no colspan/rowspan) table has an associated header, via a headers attribute, an implicit column <th> above it, or an implicit row <th> to its left.
1846
+
1847
+ **Applies to.** `<table>` elements with at least 4 rows and at least 4 columns (a "large" table, where implicit row/column header association is useful; small tables are usually self-evident), and with NO `colspan`/`rowspan` anywhere in the table.
1848
+
1849
+ **Expectation.**
1850
+
1851
+ Every `<td>` has an associated header, via one of:
1852
+
1853
+ - a non-empty `headers` attribute (trusted here; whether it resolves to real `<th>` ids is `table-headers-attr-valid`'s concern, not this rule's), OR
1854
+ - an implicit column header: some `<th>` in the same column, in an earlier row, OR
1855
+ - an implicit row header: some `<th>` earlier in the same row.
1856
+
1857
+ ### `textbox-name-present`
1858
+
1859
+ **Textboxes have an accessible name**
1860
+
1861
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1862
+
1863
+ Checks that elements with role="textbox" expose a non-empty accessible name.
1864
+
1865
+ **Applies to.** Applies to elements carrying role="textbox" (the attribute must name that role alone, not a fallback list) that are included in the accessibility tree. An element with the matching implicit role but no role attribute is out of scope.
1866
+
1867
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, or from title. role="textbox" is name-from-author-only, so subtree text is never accepted: text sitting inside a custom textbox widget is not reliably exposed as its name. On a labelable element (<input role="textbox">) an associated <label> counts as well.
1868
+
1869
+ ### `tooltip-name-present`
1870
+
1871
+ **Tooltips have an accessible name**
1872
+
1873
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1874
+
1875
+ Checks that elements with role="tooltip" expose a non-empty accessible name.
1876
+
1877
+ **Applies to.** Applies to elements carrying role="tooltip" (the attribute must name that role alone, not a fallback list) that are included in the accessibility tree. An element with the matching implicit role but no role attribute is out of scope.
1878
+
1879
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, from title, or, role="tooltip" being name-from-content, from its own subtree text, where a descendant's own name (an <img alt>, aria-label or title) counts as that descendant's contribution rather than only its text nodes.
1880
+
1881
+ ### `treeitem-name-present`
1882
+
1883
+ **Tree items have an accessible name**
1884
+
1885
+ automatic · WCAG 4.1.2 (A) · confidence high · default severity serious
1886
+
1887
+ Checks that elements with role="treeitem" expose a non-empty accessible name.
1888
+
1889
+ **Applies to.** Applies to elements carrying role="treeitem" (the attribute must name that role alone, not a fallback list) that are included in the accessibility tree. An element with the matching implicit role but no role attribute is out of scope.
1890
+
1891
+ **Expectation.** The element has a non-empty accessible name from aria-label, from an aria-labelledby that resolves to non-empty text, from title, or, role="treeitem" being name-from-content, from its own subtree text, where a descendant's own name (an <img alt>, aria-label or title) counts as that descendant's contribution rather than only its text nodes.
1892
+
1893
+ ### `valid-lang`
1894
+
1895
+ **Element lang attribute must be syntactically valid**
1896
+
1897
+ automatic · WCAG 3.1.2 (AA) · confidence high · default severity moderate
1898
+
1899
+ Checks that any element (other than the root <html>) with a non-empty lang attribute uses a syntactically valid language tag.
1900
+
1901
+ **Applies to.**
1902
+
1903
+ Applies to any element other than the root <html> with a non-empty lang attribute AND at least some non-whitespace "governed text" that actually inherits its language from that element, per ACT de46e4:
1904
+
1905
+ - Descendant text/alt is governed by the nearest lang-carrying ancestor only, a nested descendant with its own non-empty lang re-scopes everything inside it, so that subtree no longer counts toward the outer element's applicability (it counts toward the nested element's own, if that one is also being evaluated).
1906
+ - A non-empty alt attribute on img/area/input[type=image] counts as governed text, the same as a text node.
1907
+ - Text (or alt) that CSS keeps out of the render tree (display:none, the hidden attribute, ...) does not count. aria-hidden and offscreen positioning do NOT exempt text, per ACT's own failed examples for both, only actual non-rendering does.
1908
+
1909
+ **Expectation.** The lang value matches a valid BCP47 language-tag syntax. WCAG 3.1.2 (Language of Parts) requires that when a passage's language differs from the page's default, it is identified programmatically. An invalid tag fails to identify a real language at all.
1910
+
1911
+ ### `video-caption`
1912
+
1913
+ **Prerecorded video should provide a captions track**
1914
+
1915
+ manual · WCAG 1.2.2 (A) · confidence low · default severity moderate
1916
+
1917
+ Flags <video> elements with no <track kind="captions"|"subtitles"> child, for manual review of whether the video has an audio track that needs captions.
1918
+
1919
+ **Applies to.** Any <video> element in the composed DOM.
1920
+
1921
+ **Expectation.** SC 1.2.2 requires captions for prerecorded synchronized media, but only when the video actually has an audio track that conveys information (a silent/decorative video needs none), which cannot be verified from static markup alone (jsdom does not decode media). This rule is therefore `type: 'manual'` (cantTell-capped, never fail), matching the precedent set by `media-alternative-transcript-evidence` for the same class of "normatively mapped but not statically verifiable" gap. A <video> with a `<track kind="captions">` (or `kind="subtitles"`, commonly used interchangeably in the wild even though captions and subtitles serve technically distinct purposes) whose `src` is non-empty is not flagged; everything else is flagged for human review.
1922
+
1923
+ ### `video-poster-text-alternative-present`
1924
+
1925
+ **<video> poster must have a text alternative**
1926
+
1927
+ automatic · WCAG 1.1.1 (A) · confidence medium · default severity serious
1928
+
1929
+ Checks that <video> elements with a poster image provide a text alternative (accessible name).
1930
+
1931
+ **Applies to.** Applies to <video> elements that: 1) have a non-empty poster attribute, AND 2) are exposed to assistive technologies (per engine eligibility checks). Elements otherwise hidden from the accessibility tree remain applicable if they are tabbable or referenced by IDREF relationships (per eligibility checks). Videos with role="presentation" or role="none" are excluded only when they are not focusable.
1932
+
1933
+ **Expectation.**
1934
+
1935
+ Each applicable <video> element provides a text alternative for the poster image, via:
1936
+
1937
+ - an accessible name (aria-label / aria-labelledby / title).
1938
+
1939
+ Between-tag fallback content inside <video> is NOT accepted: it is only rendered by browsers that don't support <video>, so it is not reliably exposed to assistive technologies in practice. <video> is also not a labelable element, so native <label for="..."> associations are not accepted either.