@a11yfred/neighbor 1.0.4 → 2.0.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.
package/RULES-CSS.md CHANGED
@@ -1,6 +1,6 @@
1
- # @a11yfred/neighbor - CSS Rules
1
+ # @a11yfred/neighbor: CSS Rules
2
2
 
3
- Stylelint rules for CSS accessibility.
3
+ Stylelint rules that check your CSS for accessibility problems.
4
4
 
5
5
  → [Markup rules](RULES-MARKUP.md) · [Content rules](RULES-CONTENT.md) · [Back to RULES.md](RULES.md)
6
6
 
@@ -8,28 +8,50 @@ Stylelint rules for CSS accessibility.
8
8
 
9
9
  | Source | Reference |
10
10
  | --- | --- |
11
+ | double-great/stylelint-a11y | [github.com/double-great/stylelint-a11y](https://github.com/double-great/stylelint-a11y) |
12
+ | Eric Eggert | [yatil.net](https://yatil.net) - forced colors and focus patterns |
13
+ | MDN Web Docs | [forced-color-adjust](https://developer.mozilla.org/en-US/docs/Web/CSS/forced-color-adjust) |
14
+ | WCAG 2.1 SC 1.4.11 | [Non-text Contrast](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast) |
11
15
  | WCAG 2.1 SC 1.4.3 | [Contrast (Minimum)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum) |
12
16
  | WCAG 2.1 SC 1.4.4 | [Resize Text](https://www.w3.org/WAI/WCAG21/Understanding/resize-text) |
13
- | WCAG 2.1 SC 1.4.11 | [Non-text Contrast](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast) |
14
17
  | WCAG 2.1 SC 2.3.3 | [Animation from Interactions](https://www.w3.org/WAI/WCAG21/Understanding/animation-from-interactions) |
15
18
  | WCAG 2.1 SC 2.4.7 | [Focus Visible](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible) |
16
- | Eric Eggert | [yatil.net](https://yatil.net) - forced colors and focus patterns |
17
- | MDN Web Docs | [forced-color-adjust](https://developer.mozilla.org/en-US/docs/Web/CSS/forced-color-adjust) |
18
- | double-great/stylelint-a11y | [github.com/double-great/stylelint-a11y](https://github.com/double-great/stylelint-a11y) |
19
19
 
20
20
  ---
21
21
 
22
22
  ## Rules
23
23
 
24
- All CSS rules use the `neighbor/` namespace and ship from `@a11yfred/neighbor` (the default entry point) and `@a11yfred/neighbor/stylelint`.
24
+ All CSS rules start with `neighbor/`. You can use them from `@a11yfred/neighbor` or `@a11yfred/neighbor/stylelint`.
25
+
26
+ ### Errors (you must fix these)
27
+
28
+ These rules flag issues that objectively break WCAG requirements and block users from accessing your content. By default, Stylelint rules are configured as errors unless you explicitly set their severity to "warning".
25
29
 
26
- ### Warnings - on by default
30
+ | Rule | What it finds | WCAG SC |
31
+ | --- | --- | --- |
32
+ | `neighbor/no-forced-colors-none` | Using `forced-color-adjust: none` inside `@media (forced-colors)`. This blocks Windows High Contrast Mode, which hurts users with low vision. | [SC 1.4.3](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum) / [SC 1.4.11](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast) |
33
+ | `neighbor/no-outline-none` | Using `outline: none` or `outline: 0` without a `:focus` selector. This hides the focus ring for keyboard users. | [SC 2.4.7](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible) |
34
+ | `neighbor/no-text-justify` | Using `text-align: justify`. This creates uneven spaces between words that are very difficult for users with dyslexia to read. | [SC 1.4.8](https://www.w3.org/WAI/WCAG21/Understanding/visual-presentation) |
35
+
36
+ ### Warnings (configure these in your stylelint config)
27
37
 
28
- | Rule | What it flags | WCAG SC |
38
+ | Rule | What it finds | WCAG SC |
29
39
  | --- | --- | --- |
30
- | `neighbor/user-preferences` | `opacity`, `animation`, `transition`, or alpha-channel colors used in `src/components/ui/` without a `@media (prefers-reduced-motion)`, `@media (prefers-reduced-transparency)`, or `@media (forced-colors)` counterpart | [SC 1.4.3](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum) / [SC 2.3.3](https://www.w3.org/WAI/WCAG21/Understanding/animation-from-interactions) |
31
- | `neighbor/no-outline-none` | `outline: none` or `outline: 0` in a base rule (outside a `:focus`, `:focus-visible`, or `:focus-within` selector) - removes the keyboard focus indicator for all users | [SC 2.4.7](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible) |
32
- | `neighbor/no-forced-colors-none` | `forced-color-adjust: none` inside `@media (forced-colors)` - actively opts out of Windows High Contrast Mode, removing the system-enforced visibility that users with low vision depend on | [SC 1.4.3](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum) / [SC 1.4.11](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast) |
40
+ | `neighbor/no-absolute-viewport-text` | Using pure viewport units (like `font-size: 5vw`). This stops the text from getting bigger when users zoom in with their browser. | [SC 1.4.4](https://www.w3.org/WAI/WCAG21/Understanding/resize-text) |
41
+ | `neighbor/no-user-select-all-none` | Using `user-select: none` on text. This stops users from highlighting text, which breaks translation and screen reading tools. | [SC 1.4.4](https://www.w3.org/WAI/WCAG21/Understanding/resize-text) |
42
+ | `neighbor/user-preferences` | Using `opacity`, `animation`, `transition`, or see-through colors without a `@media` fallback for users who need less motion, less transparency, or forced colors. | [SC 1.4.3](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum) / [SC 2.3.3](https://www.w3.org/WAI/WCAG21/Understanding/animation-from-interactions) |
43
+
44
+ ---
45
+
46
+ ## `stylelint-a11y` Integration
47
+
48
+ If you already have [`stylelint-a11y`](https://github.com/double-great/stylelint-a11y) installed in your project, `neighbor` will automatically detect it and **turn off** the following rules to prevent duplicate warnings:
49
+
50
+ - `neighbor/no-outline-none` (handled by `a11y/no-outline-none`)
51
+ - `neighbor/no-text-justify` (handled by `a11y/no-text-align-justify`)
52
+ - Motion checks in `neighbor/user-preferences` (handled by `a11y/media-prefers-reduced-motion`)
53
+
54
+ You will still get `neighbor`'s unique checks for forced colors, text selection, and viewport sizing!
33
55
 
34
56
  ---
35
57
 
@@ -37,17 +59,17 @@ All CSS rules use the `neighbor/` namespace and ship from `@a11yfred/neighbor` (
37
59
 
38
60
  ### `neighbor/no-outline-none`
39
61
 
40
- The rule allows `outline: none` inside `:focus`, `:focus-visible`, and `:focus-within` selectors - those are intentional restylings, not removals. The pattern for programmatic-focus-only targets (skip-link destinations, dialog headings) is:
62
+ This rule allows `outline: none` inside `:focus`, `:focus-visible`, and `:focus-within`. This is because you are usually changing the style, not removing it completely. A common pattern for elements focused by JavaScript is:
41
63
 
42
64
  ```css
43
65
  :focus:not(:focus-visible) { outline: none }
44
66
  ```
45
67
 
46
- This suppresses the visible ring for JS `.focus()` calls while preserving it for keyboard-initiated focus. That pattern is not flagged.
68
+ This hides the focus ring when JavaScript focuses an element, but keeps it when a user uses the keyboard. This rule will not complain about this pattern.
47
69
 
48
70
  ### `neighbor/no-forced-colors-none`
49
71
 
50
- `forced-color-adjust: none` has a small number of valid uses (color pickers, custom border tricks) when placed *outside* a `@media (forced-colors)` block - those are not flagged. The rule only fires inside the media query, where the intent is explicitly to cancel High Contrast Mode for an element that needs it most.
72
+ Using `forced-color-adjust: none` is sometimes okay (like for color pickers) if it is *outside* a `@media (forced-colors)` block. This rule will only complain if you use it inside the media query, because that means you are trying to turn off High Contrast Mode.
51
73
 
52
74
  ---
53
75
 
@@ -55,7 +77,7 @@ This suppresses the visible ring for JS `.focus()` calls while preserving it for
55
77
 
56
78
  | Rule | Reason rejected |
57
79
  | --- | --- |
58
- | `prefer-focus-visible` | `:focus` alone satisfies [SC 2.4.7](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible); flagging it without `:focus-visible` fires constantly on legitimate code |
59
- | `no-fixed-font-size-px` | Browser zoom satisfies [SC 1.4.4](https://www.w3.org/WAI/WCAG21/Understanding/resize-text) regardless of unit; WCAG is ambiguous on this; very high false-positive rate |
60
- | `font-size-is-readable` / `no-spread-text` | No universal threshold - highly context-dependent; high false-positive rate on real design systems |
61
- | `no-forced-colors-none` (global, not scoped to media query) | Legitimate narrow uses exist; rule is scoped to `@media (forced-colors)` blocks only |
80
+ | `font-size-is-readable` / `no-spread-text` | Readable font size changes based on design. We cannot make a rule that works everywhere. |
81
+ | `no-fixed-font-size-px` | Browser zoom works with `px`. WCAG does not clearly forbid `px`. Complaining about `px` gives too many false errors. |
82
+ | `no-forced-colors-none` (global, not scoped to media query) | There are valid reasons to use this globally. We only check inside the `@media (forced-colors)` block. |
83
+ | `prefer-focus-visible` | Using `:focus` is enough for WCAG. Complaining about it gives too many false errors. |
package/RULES-MARKUP.md CHANGED
@@ -1,6 +1,6 @@
1
- # @a11yfred/neighbor - Markup Rules
1
+ # @a11yfred/neighbor: Markup Rules
2
2
 
3
- ESLint rules for React / JSX, Vue SFCs, and Angular templates.
3
+ ESLint rules for React, Remix, Vue, Angular, Lit, and plain HTML.
4
4
 
5
5
  → [CSS rules](RULES-CSS.md) · [Content rules](RULES-CONTENT.md) · [Back to RULES.md](RULES.md)
6
6
 
@@ -9,132 +9,207 @@ ESLint rules for React / JSX, Vue SFCs, and Angular templates.
9
9
  | Source | Reference |
10
10
  | --- | --- |
11
11
  | Adrian Roselli | [adrianroselli.com](https://adrianroselli.com) |
12
+ | ARIA 1.2 spec | [w3.org/TR/wai-aria-1.2](https://www.w3.org/TR/wai-aria-1.2/) |
13
+ | Deque / axe-core | deque.com: rule concepts reimplemented independently under MPL-2.0 |
14
+ | Eric Eggert | [yatil.net](https://yatil.net) |
12
15
  | Heydon Pickering | [heydonworks.com](https://heydonworks.com), [inclusive-components.design](https://inclusive-components.design) |
13
- | Scott O'Hara | [scottohara.me](https://scottohara.me) |
14
- | Patrick Lauke | [splintered.co.uk](https://splintered.co.uk), [patrickhlauke.github.io/aria](https://patrickhlauke.github.io/aria) |
16
+ | HTML Living Standard | [html.spec.whatwg.org](https://html.spec.whatwg.org/) |
15
17
  | Karl Groves | [karlgroves.com](https://karlgroves.com) |
16
18
  | Marcy Sutton | [marcysutton.com](https://marcysutton.com) |
17
- | Eric Eggert | [yatil.net](https://yatil.net) |
19
+ | Patrick Lauke | [splintered.co.uk](https://splintered.co.uk), [patrickhlauke.github.io/aria](https://patrickhlauke.github.io/aria) |
20
+ | Scott O'Hara | [scottohara.me](https://scottohara.me) |
18
21
  | WAI-ARIA APG | [w3.org/WAI/ARIA/apg](https://www.w3.org/WAI/ARIA/apg/) |
19
- | ARIA 1.2 spec | [w3.org/TR/wai-aria-1.2](https://www.w3.org/TR/wai-aria-1.2/) |
20
- | WebAIM Million | [webaim.org/projects/million](https://webaim.org/projects/million/) |
21
- | Deque / axe-core | deque.com - rule concepts reimplemented independently under MPL-2.0 |
22
22
  | WCAG 2.1 | [w3.org/TR/WCAG21](https://www.w3.org/TR/WCAG21/) |
23
23
  | WCAG 2.2 | [w3.org/TR/WCAG22](https://www.w3.org/TR/WCAG22/) |
24
- | HTML Living Standard | [html.spec.whatwg.org](https://html.spec.whatwg.org/) |
24
+ | WebAIM Million | [webaim.org/projects/million](https://webaim.org/projects/million/) |
25
25
 
26
26
  ---
27
27
 
28
- ## Core rules - all frameworks
28
+ ## Core rules: all frameworks
29
29
 
30
- All rules run on React, Vue, and Angular unless noted.
30
+ All rules run on React, Remix, Vue, Angular, and Web Components unless noted.
31
31
 
32
- ### Errors - definite breakage or phantom controls
32
+ ### Errors (you must fix these)
33
33
 
34
- | Rule | What it flags | Source |
34
+ | Rule | What it finds | Source |
35
35
  | --- | --- | --- |
36
- | `no-aria-label-on-generic` | `aria-label`/`aria-labelledby` on `<div>`, `<span>`, `<p>` with no `role` - AT ignores it | Roselli / O'Hara |
37
- | `no-assertive-live-overuse` | `aria-live="assertive"` without `role="alert"` - interrupts user unexpectedly | [APG](https://www.w3.org/WAI/ARIA/apg/) / Sutton / Eggert |
38
- | `no-unblocked-aria-disabled` | `aria-disabled="true"` on an interactive element that still has an `onClick` - clicks still fire | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
39
- | `no-roles-without-name` | `role="region/dialog/alertdialog/application/marquee/searchbox"` without `aria-label`/`aria-labelledby` | [APG](https://www.w3.org/WAI/ARIA/apg/) / [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
40
- | `no-group-without-name` | `role="group"` containing form controls without an accessible name | [APG](https://www.w3.org/WAI/ARIA/apg/) / Groves - [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
41
- | `no-presentation-on-focusable` | `role="presentation"/"none"` on a focusable element - phantom control | Roselli / Lauke / O'Hara - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
42
- | `no-log-with-interactive-children` | Interactive elements nested inside `role="log"` | [APG: Log Role](https://www.w3.org/WAI/ARIA/apg/patterns/) |
43
- | `no-aria-hidden-in-link` | `<a>` whose only content is `aria-hidden` elements - phantom link with no name | Roselli - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
44
- | `no-redundant-aria-hidden-with-presentation` | `aria-hidden="true"` combined with `role="none"/"presentation"` - redundant | O'Hara |
45
- | `no-aria-owns-on-void` | `aria-owns` on void elements (`<img>`, `<input>`, `<br>`, etc.) - meaningless | O'Hara / [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
46
- | `no-title-as-label` | `title` attribute as the sole accessible name on an `<input>` - not keyboard accessible | Groves / O'Hara - [SC 2.4.6](https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels) |
47
- | `no-tabs-without-structure` | `role="tab"` without `aria-selected`; `role="tabpanel"` without `aria-labelledby`; `role="tablist"` without an accessible name | [APG: Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
48
- | `no-positive-tabindex` | `tabIndex` value greater than 0 - breaks natural DOM tab order | WebAIM / Lauke - [SC 2.4.3](https://www.w3.org/WAI/WCAG21/Understanding/focus-order) |
49
- | `no-autoplay-without-controls` | `<video>`/`<audio autoPlay>` without `controls` | [SC 1.4.2](https://www.w3.org/WAI/WCAG21/Understanding/audio-control) |
50
- | `no-heading-inside-interactive` | Heading elements (`<h1>`–`<h6>`) nested inside `<button>`, `<a>`, or interactive roles | Roselli / Pickering - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
51
- | `no-placeholder-only` | `<input placeholder>` with no `aria-label`, `aria-labelledby`, or paired `<label>` - WebAIM Million #3 failure | [WebAIM Million](https://webaim.org/projects/million/) - [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
52
- | `no-empty-button` | `<button>` with only `aria-hidden` children and no accessible name | [WebAIM Million](https://webaim.org/projects/million/) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
53
- | `no-image-role-without-name` | `role="img"` without `aria-label`/`aria-labelledby` | [APG](https://www.w3.org/WAI/ARIA/apg/) / O'Hara - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
54
- | `no-spinbutton-without-range` | `role="spinbutton"` missing `aria-valuenow`, `aria-valuemin`, or `aria-valuemax` | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Spinbutton](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/) |
55
- | `no-slider-without-range` | `role="slider"` missing `aria-valuenow`, `aria-valuemin`, or `aria-valuemax` | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) |
56
- | `no-combobox-without-expanded` | `role="combobox"` without `aria-expanded` | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) |
57
- | `no-mouse-only-events` | `onMouseEnter`/`onMouseLeave`/`onMouseOver`/`onMouseOut` without `onFocus`/`onBlur` equivalents | [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
58
- | `no-listbox-without-option` | `role="listbox"` with no `role="option"` children | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Listbox](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) |
59
- | `no-tree-without-treeitem` | `role="tree"` with no `role="treeitem"` children | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Tree View](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) |
60
- | `no-feed-without-article` | `role="feed"` with no `role="article"` children | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Feed](https://www.w3.org/WAI/ARIA/apg/patterns/feed/) |
61
- | `no-aria-activedescendant-without-id` | `aria-activedescendant` with an empty or missing static ID | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
62
- | `no-duplicate-id` | Duplicate `id` values on elements referenced by `aria-labelledby`/`describedby`/`controls`/`owns`/`activedescendant` | [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) / [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
63
- | `no-summary-without-details` | `<summary>` outside `<details>` - phantom interactive element | [HTML spec](https://html.spec.whatwg.org/multipage/interactive-elements.html#the-summary-element) - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
64
- | `no-aria-required-on-non-form` | `aria-required` on an element whose role doesn't support it - AT ignores it | [ARIA 1.2 §6.6.9](https://www.w3.org/TR/wai-aria-1.2/#aria-required) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
65
- | `no-input-type-invalid` | `<input type="X">` with an invalid type - silently falls back to `type="text"` | [HTML spec §4.10.18](https://html.spec.whatwg.org/multipage/input.html#the-input-element) - [SC 1.3.5](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose) |
66
- | `no-labelledby-missing-target` | `aria-labelledby`/`describedby`/`controls`/`owns`/`activedescendant` referencing an `id` that doesn't exist in the file | [ARIA 1.2 §6.2.4](https://www.w3.org/TR/wai-aria-1.2/#mapping_additional_nd_name) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
67
- | `no-dynamic-content-without-live` | `dangerouslySetInnerHTML` / `v-html` / `[innerHTML]` on an element outside a live region | [SC 4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
68
- | `form-field-multiple-labels` | Multiple `<label for="…">` elements targeting the same input | [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
69
- | `no-empty-table-header` | `<th>` or `role="columnheader"/"rowheader"` with no accessible text or `aria-label` | [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
70
-
71
- ### Warnings - on by default
72
-
73
- | Rule | What it flags | Source |
36
+ | `form-field-multiple-labels` | More than one `<label>` pointing to the same `<input>`. | [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
37
+ | `no-aria-activedescendant-without-id` | `aria-activedescendant` without a valid ID. | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
38
+ | `no-aria-hidden-in-link` | A link (`<a>`) that only contains hidden elements. It has no name. | Roselli - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
39
+ | `no-aria-hidden-on-main` | Using `aria-hidden="true"` on `<body>` or `<main>`. This hides your whole app. | [APG](https://www.w3.org/WAI/ARIA/apg/) |
40
+ | `no-aria-label-on-generic` | `aria-label` or `aria-labelledby` on `<div>`, `<span>`, or `<p>` without a `role`. Screen readers ignore this. | Roselli / O'Hara |
41
+ | `no-aria-owns-on-void` | Using `aria-owns` on elements that cannot have children (like `<img>` or `<input>`). | O'Hara / [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
42
+ | `no-aria-required-on-non-form` | Using `aria-required` on something that is not a form input. | [ARIA 1.2 §6.6.9](https://www.w3.org/TR/wai-aria-1.2/#aria-required) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
43
+ | `no-assertive-live-overuse` | `aria-live="assertive"` without `role="alert"`. This interrupts the user when they do not expect it. | [APG](https://www.w3.org/WAI/ARIA/apg/) / Sutton / Eggert |
44
+ | `no-autoplay-without-controls` | Autoplaying video or audio without giving the user controls to stop it. | [SC 1.4.2](https://www.w3.org/WAI/WCAG21/Understanding/audio-control) |
45
+ | `no-combobox-without-expanded` | `role="combobox"` without `aria-expanded`. | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) |
46
+ | `no-disabled-and-aria-disabled` | Using both `disabled` and `aria-disabled` at the same time. | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
47
+ | `no-duplicate-id` | Using the same `id` twice when ARIA is trying to point to it. | [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) / [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
48
+ | `no-dynamic-content-without-live` | Adding HTML dynamically without using a live region to tell the screen reader. | [SC 4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
49
+ | `no-empty-button` | A `<button>` that only has hidden children and no name. | [WebAIM Million](https://webaim.org/projects/million/) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
50
+ | `no-empty-table-header` | A table header (`<th>`) with no text. | [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
51
+ | `no-feed-without-article` | `role="feed"` without any `role="article"` inside it. | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Feed](https://www.w3.org/WAI/ARIA/apg/patterns/feed/) |
52
+ | `no-group-without-name` | `role="group"` with form inputs, but no accessible name. | [APG](https://www.w3.org/WAI/ARIA/apg/) / Groves - [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
53
+ | `no-heading-inside-interactive` | Putting a heading inside a button or link. | Roselli / Pickering - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
54
+ | `no-image-role-without-name` | `role="img"` without an accessible name. | [APG](https://www.w3.org/WAI/ARIA/apg/) / O'Hara - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
55
+ | `no-input-type-invalid` | Using an `<input>` type that does not exist. | [HTML spec §4.10.18](https://html.spec.whatwg.org/multipage/input.html#the-input-element) - [SC 1.3.5](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose) |
56
+ | `no-labelledby-missing-target` | ARIA pointing to an `id` that does not exist. | [ARIA 1.2 §6.2.4](https://www.w3.org/TR/wai-aria-1.2/#mapping_additional_nd_name) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
57
+ | `no-listbox-without-option` | `role="listbox"` without any `role="option"` inside it. | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Listbox](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) |
58
+ | `no-log-with-interactive-children` | Buttons or links inside `role="log"`. | [APG: Log Role](https://www.w3.org/WAI/ARIA/apg/patterns/) |
59
+ | `no-meter-without-valuenow` | `role="meter"` missing `aria-valuenow`. | [APG: Meter](https://www.w3.org/WAI/ARIA/apg/patterns/meter/) |
60
+ | `no-mouse-only-events` | Using mouse events (like `onMouseEnter`) without adding keyboard events (like `onFocus`). | [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
61
+ | `no-placeholder-only` | Using only a `placeholder` to label an `<input>`. | [WebAIM Million](https://webaim.org/projects/million/) - [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
62
+ | `no-positive-tabindex` | Using a `tabIndex` greater than 0. This breaks the normal keyboard tab order. | WebAIM / Lauke - [SC 2.4.3](https://www.w3.org/WAI/WCAG21/Understanding/focus-order) |
63
+ | `no-presentation-on-focusable` | Using `role="presentation"` on something you can focus on. | Roselli / Lauke / O'Hara - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
64
+ | `no-roles-without-name` | Using `role="dialog"` or similar roles without giving them an accessible name. | [APG](https://www.w3.org/WAI/ARIA/apg/) / [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
65
+ | `no-slider-without-range` | `role="slider"` missing range values (`aria-valuenow`, etc.). | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) |
66
+ | `no-spinbutton-without-range` | `role="spinbutton"` missing range values (`aria-valuenow`, etc.). | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Spinbutton](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/) |
67
+ | `no-summary-without-details` | Using `<summary>` outside of a `<details>` element. | [HTML spec](https://html.spec.whatwg.org/multipage/interactive-elements.html#the-summary-element) - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
68
+ | `no-tabs-without-structure` | Missing pieces in a tab menu (like a tab without `aria-selected`). | [APG: Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
69
+ | `no-title-as-label` | Using only the `title` attribute to name an `<input>`. Keyboard users cannot see this. | Groves / O'Hara - [SC 2.4.6](https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels) |
70
+ | `no-toggle-without-checked` | A switch, checkbox, or radio button missing `aria-checked`. | [APG: Checkbox](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/) |
71
+ | `no-tree-without-treeitem` | `role="tree"` without any `role="treeitem"` inside it. | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Tree View](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) |
72
+ | `no-unblocked-aria-disabled` | `aria-disabled="true"` on a button or link that still has an `onClick`. The button still works even though it says it is disabled. | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
73
+
74
+ ### Warnings (these are usually bad)
75
+
76
+ | Rule | What it finds | Source |
74
77
  | --- | --- | --- |
75
- | `no-tooltip-role-misuse` | `role="tooltip"` without an `id`; or `role="tooltip"` on an interactive element | [APG: Tooltip Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
76
- | `no-menu-role-on-nav` | Menu/menubar/menuitem roles - triggers AT application-mode keyboard handling; especially wrong on `<nav>` | Roselli / Lauke / Groves - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
77
- | `no-button-type-missing` | `<button>` inside a `<form>` without an explicit `type` - defaults to `type="submit"` | [HTML spec §4.10.18](https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element) |
78
+ | `no-button-type-missing` | A `<button>` inside a `<form>` missing `type="button"` or `type="submit"`. | [HTML spec §4.10.18](https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element) |
79
+ | `no-expanded-without-controls` | `aria-expanded` without `aria-controls`. | [APG: Disclosure](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) |
80
+ | `no-menu-role-on-nav` | Using menu roles (like `role="menu"`). This changes how keyboards work and is usually wrong. | Roselli / Lauke / Groves - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
81
+ | `no-multiple-main` | Having more than one `<main>` element. | [Axe: landmark-one-main](https://dequeuniversity.com/rules/axe/4.8/landmark-one-main) |
82
+ | `no-redundant-aria-hidden-with-presentation` | Using both `aria-hidden="true"` and `role="presentation"`. You only need one. | O'Hara |
83
+ | `no-skipped-heading-levels` | Skipping heading levels (like going from `<h1>` straight to `<h3>`). | [Axe: heading-order](https://dequeuniversity.com/rules/axe/4.8/heading-order) - [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
84
+ | `no-tooltip-role-misuse` | `role="tooltip"` without an `id`, or putting it on a button/link. | [APG: Tooltip Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
78
85
 
79
- ### Off by default - opt in
86
+ > **Note on components:** `no-skipped-heading-levels` and `no-multiple-main` are only checked in a single file. They cannot promise that your headings are perfect across your entire app. To test the whole app, use a tool like `@axe-core/react`.
80
87
 
81
- These rules flag real problems but generate enough noise in typical codebases that they ship off. Enable individually.
88
+ ### Off by default (you can turn these on)
82
89
 
83
- | Rule | What it flags | Source |
90
+ These rules find real problems, but they complain a lot in most projects. You can turn them on if you want.
91
+
92
+ | Rule | What it finds | Source |
84
93
  | --- | --- | --- |
85
- | `no-application-role` | `role="application"` - disables AT browse mode | Roselli / Sutton / Lauke / [APG](https://www.w3.org/WAI/ARIA/apg/) |
86
- | `no-grid-role` | `role="grid"` - almost always wrong outside spreadsheet-like widgets | Roselli: ARIA Grid As an Anti-Pattern |
87
- | `no-aria-roledescription` | `aria-roledescription` - overrides AT role label, does not auto-translate | Roselli: Avoid aria-roledescription |
88
- | `no-aria-readonly` | `aria-readonly` - limited and inconsistent AT support | Roselli |
89
- | `no-tab-without-controls` | `role="tab"` without `aria-controls` | [APG: Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) |
90
- | `no-href-hash` | `<a href="#">` used as a button | Sutton: Links vs Buttons - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
91
- | `warn-role-alert` | `role="alert"` - prefer `role="status"` for non-urgent updates | [APG](https://www.w3.org/WAI/ARIA/apg/) / Roselli / Sutton - [SC 4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
92
- | `prefer-aria-disabled` | HTML `disabled` removes element from tab order; `aria-disabled` keeps it discoverable | Roselli: Don't Disable Form Controls - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
93
- | `no-target-blank-without-label` | `target="_blank"` without communicating the new-tab behaviour | WebAIM - [SC 3.2.2](https://www.w3.org/WAI/WCAG21/Understanding/on-input) |
94
- | `no-dialog-without-close` | `role="dialog"` or `<dialog>` without a visible close button | [APG: Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) - [SC 2.1.2](https://www.w3.org/WAI/WCAG21/Understanding/no-keyboard-trap) |
94
+ | `no-application-role` | `role="application"` (disables normal screen reader reading). | Roselli / Sutton / Lauke / [APG](https://www.w3.org/WAI/ARIA/apg/) |
95
+ | `no-aria-readonly` | `aria-readonly` (screen readers do not support this well). | Roselli |
96
+ | `no-aria-roledescription` | `aria-roledescription` (does not translate to other languages). | Roselli: Avoid aria-roledescription |
97
+ | `no-dialog-without-close` | A dialog without a close button. | [APG: Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) - [SC 2.1.2](https://www.w3.org/WAI/WCAG21/Understanding/no-keyboard-trap) |
98
+ | `no-grid-role` | `role="grid"` (almost always wrong unless building a spreadsheet). | Roselli: ARIA Grid As an Anti-Pattern |
99
+ | `no-href-hash` | Using `<a href="#">` instead of a `<button>`. | Sutton: Links vs Buttons - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
100
+ | `no-tab-without-controls` | `role="tab"` missing `aria-controls`. | [APG: Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) |
101
+ | `no-target-blank-without-label` | Using `target="_blank"` without telling the user it will open a new tab. | WebAIM - [SC 3.2.2](https://www.w3.org/WAI/WCAG21/Understanding/on-input) |
102
+ | `prefer-aria-disabled` | Using HTML `disabled` (which hides it from the keyboard). You should use `aria-disabled` instead. | Roselli: Don't Disable Form Controls - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
103
+ | `warn-role-alert` | Using `role="alert"`. You should use `role="status"` for things that are not urgent. | [APG](https://www.w3.org/WAI/ARIA/apg/) / Roselli / Sutton - [SC 4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
95
104
 
96
105
  ---
97
106
 
98
- ## Portability rules - Vue and Angular only
107
+ ## Portability rules: Vue and Angular only
99
108
 
100
109
  These rules cover gaps in `eslint-plugin-jsx-a11y` that have no equivalent in `eslint-plugin-vuejs-accessibility` or `@angular-eslint/eslint-plugin-template`. React projects get these from jsx-a11y already.
101
110
 
102
111
  | Rule | What it flags | Source |
103
112
  | --- | --- | --- |
113
+ | `no-access-key` | `accessKey` attribute - conflicts with AT and browser shortcuts | [SC 2.1.4](https://www.w3.org/WAI/WCAG21/Understanding/character-key-shortcuts) |
104
114
  | `no-anchor-ambiguous-text` | Ambiguous link text ("click here", "read more", "learn more") | [SC 2.4.4](https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context) |
105
115
  | `no-anchor-no-content` | `<a>` with no text content and no accessible name | [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
106
116
  | `no-aria-activedescendant-no-tabindex` | `aria-activedescendant` on an element without `tabindex` | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
107
- | `no-invalid-aria-prop-value` | Invalid values on ARIA state/property attributes | [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
108
117
  | `no-autocomplete-invalid` | Invalid `autocomplete` token values | [SC 1.3.5](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose) |
109
- | `no-heading-no-content` | Headings (`<h1>`–`<h6>`) with no text content | [SC 2.4.6](https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels) |
118
+ | `no-heading-no-content` | Headings (`<h1>`-`<h6>`) with no text content | [SC 2.4.6](https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels) |
110
119
  | `no-iframe-no-title` | `<iframe>` without a `title` attribute | [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
111
120
  | `no-img-redundant-alt` | Alt text containing "image", "photo", or "picture" | [SC 1.1.1](https://www.w3.org/WAI/WCAG21/Understanding/non-text-content) |
112
- | `no-access-key` | `accessKey` attribute - conflicts with AT and browser shortcuts | [SC 2.1.4](https://www.w3.org/WAI/WCAG21/Understanding/character-key-shortcuts) |
113
- | `no-noninteractive-to-interactive-role` | Non-interactive elements given interactive ARIA roles without keyboard handlers | [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) / [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
121
+ | `no-invalid-aria-prop-value` | Invalid values on ARIA state/property attributes | [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
114
122
  | `no-noninteractive-tabindex` | `tabindex` on a non-interactive element with no interactive role | [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
123
+ | `no-noninteractive-to-interactive-role` | Non-interactive elements given interactive ARIA roles without keyboard handlers | [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) / [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
124
+ | `no-role-supports-aria-props` | ARIA properties applied to roles that do not support them | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
125
+ | `no-scope-on-td` | `scope` attribute on `<td>` - only valid on `<th>` | [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
115
126
  | `prefer-semantic-element` | `<div role="button">` where a native element would be correct | [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
116
- | `no-role-supports-aria-props` | ARIA properties applied to roles that don't support them | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
117
- | `no-scope-on-td` | `scope` attribute on `<td>` - only valid on `<th>` | [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
127
+
128
+ ## Framework-specific rules: React, Vue, Angular, Remix, Lit
129
+
130
+ These rules apply only to specific frameworks using their respective parser plugins.
131
+
132
+ | Rule | Severity | What it finds |
133
+ | --- | --- | --- |
134
+ | `angular-host-a11y` | error | Setting `role: 'button'` on an Angular component without setting a `tabindex`. |
135
+ | `lit-no-autofocus` | error | Using `autofocus` inside a Lit `html` template. |
136
+ | `remix-route-title-missing` | error | Remix route missing a `title` in its `meta` export. |
137
+ | `vue-click-key-events` | error | Adding `@click` to something that is not a button, but forgetting to add keyboard events. |
138
+ | `react-fragment-ruins-aria` | warn | React `<Fragment>` (or `<>`) with ARIA attributes. The attributes get deleted when the HTML is created. |
139
+ | `react-spa-focus-management` | warn | Using `useNavigate` or `<Link>` without managing focus. Keyboard users get lost when the page changes. |
140
+ | `vue-transition-live-region` | warn | `<Transition>` around a live region. Screen readers will not announce this correctly. |
141
+ | `angular-router-focus-management` | off | `<router-outlet>` without managing focus when the page changes. |
142
+ | `vue-router-focus-management` | off | `<RouterView>` without managing focus or using `aria-live`. |
143
+
144
+ ## Framework-Specific Omissions
145
+
146
+ Neighbor is designed to run alongside standard accessibility linters (like `eslint-plugin-jsx-a11y`, `eslint-plugin-vuejs-accessibility`, `@angular-eslint/eslint-plugin-template`, and `eslint-plugin-lit-a11y`).
147
+
148
+ If you have these standard linters installed, Neighbor will automatically **turn off** its own redundant base rules to prevent duplicate warnings. If you choose *not* to install the standard linters, Neighbor will keep these base rules enabled to protect your codebase.
149
+
150
+ ### Vue (`eslint-plugin-vuejs-accessibility`)
151
+
152
+ Omitted base rules: `no-heading-no-content`, `no-iframe-no-title`, `no-access-key`, `no-img-redundant-alt`, `no-anchor-no-content`, `no-invalid-aria-prop-value`, `no-role-supports-aria-props`.
153
+
154
+ ### Angular (`@angular-eslint/eslint-plugin-template`)
155
+
156
+ Omitted base rules: `no-heading-no-content`, `no-anchor-no-content`, `no-img-redundant-alt`, `no-scope-on-td`, `no-invalid-aria-prop-value`.
157
+
158
+ ### Lit (`eslint-plugin-lit-a11y`)
159
+
160
+ Omitted base rules: `no-heading-no-content`, `no-iframe-no-title`, `no-img-redundant-alt`, `no-access-key`, `no-aria-activedescendant-no-tabindex`, `no-anchor-no-content`, `no-invalid-aria-prop-value`, `no-role-supports-aria-props`, `no-scope-on-td`.
161
+
162
+ ---
163
+
164
+ ## Vue / Angular / Lit specific rules
165
+
166
+ These rules are added specifically to handle framework-specific ASTs or behaviors.
167
+
168
+ | Rule | What it finds | Framework |
169
+ | --- | --- | --- |
170
+ | `angular-host-a11y` | Angular component missing `tabindex` for interactive host role | Angular |
171
+ | `angular-router-focus-management` | SPA route without focus management | Angular |
172
+ | `lit-no-autofocus` | `autofocus` attribute used inside a Lit template | Lit |
173
+ | `no-access-key` | `accessKey` attribute | Vue, Angular, Lit |
174
+ | `no-anchor-ambiguous-text` | Ambiguous link text ("click here") | Vue, Angular |
175
+ | `no-anchor-no-content` | `<a>` with no content | Vue, Angular |
176
+ | `no-aria-activedescendant-no-tabindex` | `aria-activedescendant` without `tabindex` | Vue, Angular |
177
+ | `no-autocomplete-invalid` | Invalid `autocomplete` token | Vue, Angular |
178
+ | `no-heading-no-content` | Heading with no content | Vue, Angular |
179
+ | `no-iframe-no-title` | `<iframe>` with no `title` | Vue, Angular |
180
+ | `no-img-redundant-alt` | `<img>` alt text contains "image of" | Vue, Angular |
181
+ | `no-invalid-aria-prop-value` | Invalid ARIA attribute values | Vue, Angular |
182
+ | `no-noninteractive-tabindex` | Non-interactive element with `tabindex` | Vue, Angular |
183
+ | `no-noninteractive-to-interactive-role` | Non-interactive element with interactive role | Vue, Angular |
184
+ | `no-role-supports-aria-props` | Using an ARIA attribute not supported by the role | Vue, Angular |
185
+ | `no-scope-on-td` | `scope` on `<td>` | Vue, Angular, Lit |
186
+ | `prefer-semantic-element` | Use native HTML tags instead of roles | Vue, Angular |
187
+ | `vue-click-key-events` | `v-on:click` without key equivalent | Vue |
188
+ | `vue-router-focus-management` | SPA route without focus management | Vue |
189
+ | `vue-transition-live-region` | `<Transition>` changing live regions | Vue |
118
190
 
119
191
  ---
120
192
 
121
- ## Framework-specific rules - @ulam only
193
+ ## Framework-specific rules: @ulam only
122
194
 
123
195
  These rules are specific to the @ulam framework and activate only when @ulam-related imports are detected.
124
196
 
125
- | Rule | Severity | What it flags |
197
+ | Rule | Severity | What it finds |
126
198
  | --- | --- | --- |
127
- | `no-announce-in-render` | error | `announce()` / `clearAnnouncements()` called in a component render body or Vue setup - fires on every render, spamming screen readers. Safe contexts: `useEffect` / `onMounted` / `watch` / event handlers |
128
- | `no-hash-router-in-remix` | warn | @ulam hash router alongside `react-router` - signals an incomplete Remix migration |
129
- | `no-use-page-title-in-remix` | warn | `usePageTitle()` alongside `react-router` - conflicts with Remix's declarative `meta` export |
199
+ | `no-announce-in-render` | error | Calling `announce()` during a render. This will spam screen readers. Only call it inside `useEffect`, `onMounted`, or event handlers. |
200
+ | `no-hash-router-in-remix` | warn | Using the @ulam hash router with `react-router`. This means your Remix migration is not finished. |
201
+ | `no-use-page-title-in-remix` | warn | Using `usePageTitle()` with `react-router`. This breaks Remix's `meta` export. |
130
202
 
131
- The `no-announce-in-render` rule runs in all three plugins with safe contexts per framework:
203
+ The `no-announce-in-render` rule runs in React, Vue, and Angular plugins with safe contexts per framework:
132
204
 
133
205
  - **React:** `useEffect`, `useLayoutEffect`, `useCallback`, `useMemo`, and event handlers
134
206
  - **Vue:** `onMounted`, `onUpdated`, `watch`, `watchEffect`, `nextTick`, and their variants
135
207
  - **Angular:** `ngOnInit`, `ngAfterViewInit`, `ngAfterContentInit`, `ngOnChanges`, `ngDoCheck`, and class method event handlers
136
208
 
137
- **Known Angular limitation:** Angular's template parser does not attach parent pointers to AST nodes. Rules that walk up the tree (`no-summary-without-details`, `no-button-type-missing`, `no-log-with-interactive-children`, `no-menu-role-on-nav`, `no-heading-inside-interactive`) will silently pass in Angular templates. The `no-dynamic-content-without-live` rule only checks the element itself (no ancestor walk) in Angular.
209
+ **Known problems with parsers:**
210
+
211
+ - **Angular templates:** The parser does not let us look up the tree. Some rules that need to look at parent elements will not work in Angular.
212
+ - **Web Components:** The `@html-eslint/parser` has the same problem. Rules cannot look at parent elements.
138
213
 
139
214
  ---
140
215
 
@@ -142,15 +217,15 @@ The `no-announce-in-render` rule runs in all three plugins with safe contexts pe
142
217
 
143
218
  | Rule | Reason rejected |
144
219
  | --- | --- |
145
- | `no-aria-controls` | Support improved substantially since Pickering's 2014 post; APG *requires* it in the tabs pattern - conflicted with `no-tabs-without-structure` |
146
- | `no-aria-label-on-link` | `aria-label` on `<a>` is the correct technique for ambiguous link text; can't detect the bad case (overriding good visible text) statically |
147
- | `no-aria-live-on-carousel` | Class-name heuristic - `carousel` in a class doesn't mean auto-advancing; too many false positives |
148
- | `no-figure-role-without-label` | `role="figure"` on `<figure>` is redundant (element already has the role implicitly); flags the wrong thing |
149
- | `no-scrollable-without-focusable` | Class-name heuristic for scroll behaviour - can't read CSS from static analysis |
150
- | `no-empty-heading` | Covered by jsx-a11y recommended |
151
- | `aria-required-on-required-form-control` | AT already reads native `required`; adding `aria-required` is redundant, not required |
152
- | `require-menu-owned-menuitem` / `require-listbox-owned-option` | Component-based code renders children conditionally - fires constantly on empty/loading states |
153
- | `no-aria-owns-circular` | Cross-file ID graph required; vanishingly rare in practice |
154
- | `no-dialog-without-modal` | Non-modal dialogs are a valid APG pattern; too much false-positive risk |
155
- | `no-generated-content-text` | Decorative generated content is extremely common; can't distinguish decorative from meaningful statically |
156
- | DevTools console output for accessibility | Requires runtime execution; belongs in a browser devtools extension, not a static linter |
220
+ | `aria-required-on-required-form-control` | Screen readers already know what HTML `required` means. Adding `aria-required` is not needed. |
221
+ | DevTools console output for accessibility | This belongs in a browser extension, not a code linter. |
222
+ | `no-aria-controls` | Screen reader support is better now. It is required for tabs. |
223
+ | `no-aria-label-on-link` | Using `aria-label` on `<a>` is the right way to fix bad link text. A linter cannot check if you are doing it correctly. |
224
+ | `no-aria-live-on-carousel` | Just because a class says `carousel` does not mean it moves by itself. This gives too many false errors. |
225
+ | `no-aria-owns-circular` | This requires checking across different files, which is too hard for a simple linter. |
226
+ | `no-dialog-without-modal` | Non-modal dialogs are allowed. This would give too many false errors. |
227
+ | `no-empty-heading` | Another plugin (`jsx-a11y`) already checks this. |
228
+ | `no-figure-role-without-label` | Putting `role="figure"` on a `<figure>` is redundant. It flags the wrong problem. |
229
+ | `no-generated-content-text` | We cannot tell if CSS generated content is decorative or important. |
230
+ | `no-scrollable-without-focusable` | We cannot read CSS to see if an element scrolls. |
231
+ | `require-menu-owned-menuitem` / `require-listbox-owned-option` | In React/Vue, children are often hidden while loading. This would give too many false errors. |