@a11yfred/neighbor 1.1.2 → 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-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,133 +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
- | `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) |
71
- | `no-disabled-and-aria-disabled` | Element has both `disabled` and `aria-disabled` - causes conflicting states in assistive tech | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
72
-
73
- ### Warnings - on by default
74
-
75
- | 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 |
76
77
  | --- | --- | --- |
77
- | `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) |
78
- | `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) |
79
- | `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) |
80
85
 
81
- ### 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`.
82
87
 
83
- 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)
84
89
 
85
- | 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 |
86
93
  | --- | --- | --- |
87
- | `no-application-role` | `role="application"` - disables AT browse mode | Roselli / Sutton / Lauke / [APG](https://www.w3.org/WAI/ARIA/apg/) |
88
- | `no-grid-role` | `role="grid"` - almost always wrong outside spreadsheet-like widgets | Roselli: ARIA Grid As an Anti-Pattern |
89
- | `no-aria-roledescription` | `aria-roledescription` - overrides AT role label, does not auto-translate | Roselli: Avoid aria-roledescription |
90
- | `no-aria-readonly` | `aria-readonly` - limited and inconsistent AT support | Roselli |
91
- | `no-tab-without-controls` | `role="tab"` without `aria-controls` | [APG: Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) |
92
- | `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) |
93
- | `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) |
94
- | `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) |
95
- | `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) |
96
104
 
97
105
  ---
98
106
 
99
- ## Portability rules - Vue and Angular only
107
+ ## Portability rules: Vue and Angular only
100
108
 
101
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.
102
110
 
103
111
  | Rule | What it flags | Source |
104
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) |
105
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) |
106
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) |
107
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/) |
108
- | `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) |
109
117
  | `no-autocomplete-invalid` | Invalid `autocomplete` token values | [SC 1.3.5](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose) |
110
- | `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) |
111
119
  | `no-iframe-no-title` | `<iframe>` without a `title` attribute | [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
112
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) |
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) |
114
- | `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) |
115
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) |
116
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) |
117
- | `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/) |
118
- | `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 |
119
190
 
120
191
  ---
121
192
 
122
- ## Framework-specific rules - @ulam only
193
+ ## Framework-specific rules: @ulam only
123
194
 
124
195
  These rules are specific to the @ulam framework and activate only when @ulam-related imports are detected.
125
196
 
126
- | Rule | Severity | What it flags |
197
+ | Rule | Severity | What it finds |
127
198
  | --- | --- | --- |
128
- | `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 |
129
- | `no-hash-router-in-remix` | warn | @ulam hash router alongside `react-router` - signals an incomplete Remix migration |
130
- | `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. |
131
202
 
132
- 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:
133
204
 
134
205
  - **React:** `useEffect`, `useLayoutEffect`, `useCallback`, `useMemo`, and event handlers
135
206
  - **Vue:** `onMounted`, `onUpdated`, `watch`, `watchEffect`, `nextTick`, and their variants
136
207
  - **Angular:** `ngOnInit`, `ngAfterViewInit`, `ngAfterContentInit`, `ngOnChanges`, `ngDoCheck`, and class method event handlers
137
208
 
138
- **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.
139
213
 
140
214
  ---
141
215
 
@@ -143,15 +217,15 @@ The `no-announce-in-render` rule runs in all three plugins with safe contexts pe
143
217
 
144
218
  | Rule | Reason rejected |
145
219
  | --- | --- |
146
- | `no-aria-controls` | Support improved substantially since Pickering's 2014 post; APG *requires* it in the tabs pattern - conflicted with `no-tabs-without-structure` |
147
- | `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 |
148
- | `no-aria-live-on-carousel` | Class-name heuristic - `carousel` in a class doesn't mean auto-advancing; too many false positives |
149
- | `no-figure-role-without-label` | `role="figure"` on `<figure>` is redundant (element already has the role implicitly); flags the wrong thing |
150
- | `no-scrollable-without-focusable` | Class-name heuristic for scroll behaviour - can't read CSS from static analysis |
151
- | `no-empty-heading` | Covered by jsx-a11y recommended |
152
- | `aria-required-on-required-form-control` | AT already reads native `required`; adding `aria-required` is redundant, not required |
153
- | `require-menu-owned-menuitem` / `require-listbox-owned-option` | Component-based code renders children conditionally - fires constantly on empty/loading states |
154
- | `no-aria-owns-circular` | Cross-file ID graph required; vanishingly rare in practice |
155
- | `no-dialog-without-modal` | Non-modal dialogs are a valid APG pattern; too much false-positive risk |
156
- | `no-generated-content-text` | Decorative generated content is extremely common; can't distinguish decorative from meaningful statically |
157
- | 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. |
package/RULES.md CHANGED
@@ -1,24 +1,25 @@
1
- # @a11yfred/neighbor - Rule Index
1
+ # @a11yfred/neighbor: Rule Index
2
2
 
3
- Neighbor ships rules across three separate domains. Each has its own reference page.
3
+ Neighbor has rules for four different areas. Each area has its own page.
4
4
 
5
- | Domain | Entry point | Rules page |
5
+ | Area | Setup | Rules page |
6
6
  | --- | --- | --- |
7
- | Markup | `@a11yfred/neighbor/eslint`, `/eslint-vue`, `/eslint-angular` | [RULES-MARKUP.md](RULES-MARKUP.md) |
8
- | CSS | `@a11yfred/neighbor` (default), `@a11yfred/neighbor/stylelint` | [RULES-CSS.md](RULES-CSS.md) |
9
- | Content | `@a11yfred/neighbor/content` | [RULES-CONTENT.md](RULES-CONTENT.md) |
7
+ | CSS | `@a11yfred/neighbor`, `@a11yfred/neighbor/stylelint` | [RULES-CSS.md](RULES-CSS.md) |
8
+ | HTML / Markup | `@a11yfred/neighbor/eslint`, `/eslint-vue`, `/eslint-angular`, `/webcomponents` | [RULES-MARKUP.md](RULES-MARKUP.md) |
9
+ | Native Mobile | `apps/ios-app`, `apps/android-app` | [iOS Rules](apps/ios-app/README.md) / [Android Rules](apps/android-app/README.md) |
10
+ | Text / Content | `@a11yfred/neighbor/content` | [RULES-CONTENT.md](RULES-CONTENT.md) |
10
11
 
11
12
  ---
12
13
 
13
- ## Markup rules - summary
14
+ ## Markup rules: summary
14
15
 
15
- ESLint rules that flag bad ARIA patterns, missing accessible names, keyboard traps, and structural errors in JSX, Vue SFCs, and Angular templates. Full reference → [RULES-MARKUP.md](RULES-MARKUP.md)
16
+ ESLint rules that find bad ARIA code, missing names, keyboard traps, and HTML mistakes in React, Vue, Angular, Lit, and plain HTML. Full list → [RULES-MARKUP.md](RULES-MARKUP.md)
16
17
 
17
- **Errors (definite breakage):** `no-aria-label-on-generic`, `no-assertive-live-overuse`, `no-unblocked-aria-disabled`, `no-roles-without-name`, `no-group-without-name`, `no-presentation-on-focusable`, `no-log-with-interactive-children`, `no-aria-hidden-in-link`, `no-redundant-aria-hidden-with-presentation`, `no-aria-owns-on-void`, `no-title-as-label`, `no-tabs-without-structure`, `no-positive-tabindex`, `no-autoplay-without-controls`, `no-heading-inside-interactive`, `no-placeholder-only`, `no-empty-button`, `no-image-role-without-name`, `no-spinbutton-without-range`, `no-slider-without-range`, `no-combobox-without-expanded`, `no-mouse-only-events`, `no-listbox-without-option`, `no-tree-without-treeitem`, `no-feed-without-article`, `no-aria-activedescendant-without-id`, `no-duplicate-id`, `no-summary-without-details`, `no-aria-required-on-non-form`, `no-input-type-invalid`, `no-labelledby-missing-target`, `no-dynamic-content-without-live`, `form-field-multiple-labels`, `no-empty-table-header`, `no-disabled-and-aria-disabled`, `prefer-aria-disabled`
18
+ **Errors (you must fix these):** `no-aria-label-on-generic`, `no-assertive-live-overuse`, `no-unblocked-aria-disabled`, `no-roles-without-name`, `no-group-without-name`, `no-presentation-on-focusable`, `no-log-with-interactive-children`, `no-aria-hidden-in-link`, `no-redundant-aria-hidden-with-presentation`, `no-aria-owns-on-void`, `no-title-as-label`, `no-tabs-without-structure`, `no-positive-tabindex`, `no-autoplay-without-controls`, `no-heading-inside-interactive`, `no-placeholder-only`, `no-empty-button`, `no-image-role-without-name`, `no-spinbutton-without-range`, `no-slider-without-range`, `no-combobox-without-expanded`, `no-mouse-only-events`, `no-listbox-without-option`, `no-tree-without-treeitem`, `no-feed-without-article`, `no-aria-activedescendant-without-id`, `no-duplicate-id`, `no-summary-without-details`, `no-aria-required-on-non-form`, `no-input-type-invalid`, `no-labelledby-missing-target`, `no-dynamic-content-without-live`, `form-field-multiple-labels`, `no-empty-table-header`, `no-disabled-and-aria-disabled`, `prefer-aria-disabled`
18
19
 
19
- **Warnings (on by default):** `no-tooltip-role-misuse`, `no-menu-role-on-nav`, `no-button-type-missing`
20
+ **Warnings (these are usually bad):** `no-tooltip-role-misuse`, `no-menu-role-on-nav`, `no-button-type-missing`
20
21
 
21
- **Off by default (opt in):** `no-application-role`, `no-grid-role`, `no-aria-roledescription`, `no-aria-readonly`, `no-tab-without-controls`, `no-href-hash`, `warn-role-alert`, `no-target-blank-without-label`, `no-dialog-without-close`
22
+ **Off by default (you can turn these on):** `no-application-role`, `no-grid-role`, `no-aria-roledescription`, `no-aria-readonly`, `no-tab-without-controls`, `no-href-hash`, `warn-role-alert`, `no-target-blank-without-label`, `no-dialog-without-close`
22
23
 
23
24
  **Vue / Angular only:** `no-anchor-ambiguous-text`, `no-anchor-no-content`, `no-aria-activedescendant-no-tabindex`, `no-invalid-aria-prop-value`, `no-autocomplete-invalid`, `no-heading-no-content`, `no-iframe-no-title`, `no-img-redundant-alt`, `no-access-key`, `no-noninteractive-to-interactive-role`, `no-noninteractive-tabindex`, `prefer-semantic-element`, `no-role-supports-aria-props`, `no-scope-on-td`
24
25
 
@@ -26,30 +27,48 @@ ESLint rules that flag bad ARIA patterns, missing accessible names, keyboard tra
26
27
 
27
28
  ---
28
29
 
29
- ## CSS rules - summary
30
+ ## CSS rules: summary
30
31
 
31
- Stylelint rules that flag CSS that removes focus indicators, opts out of High Contrast Mode, or fails to provide user-preference media query fallbacks. Full reference → [RULES-CSS.md](RULES-CSS.md)
32
+ Stylelint rules that find bad CSS. They check if you hide focus rings, block High Contrast Mode, or ignore user preferences for motion. Full list → [RULES-CSS.md](RULES-CSS.md)
32
33
 
33
- | Rule | What it flags |
34
+ | Rule | What it finds |
34
35
  | --- | --- |
36
+ | `neighbor/no-absolute-viewport-text` | Pure viewport units (`vw`, `vh`) for text sizing - this stops browser zoom from working |
37
+ | `neighbor/no-forced-colors-none` | `forced-color-adjust: none` inside `@media (forced-colors)` - this blocks Windows High Contrast Mode |
38
+ | `neighbor/no-outline-none` | `outline: none` outside `:focus` - this removes keyboard focus rings |
39
+ | `neighbor/no-text-justify` | `text-align: justify` - this creates uneven word spacing that is hard for dyslexic users to read |
40
+ | `neighbor/no-user-select-all-none` | `user-select: none` on text - this stops users from highlighting, copying, and translating text |
35
41
  | `neighbor/user-preferences` | Animation, motion, and transparency without `@media (prefers-*)` fallbacks |
36
- | `neighbor/no-outline-none` | `outline: none` outside `:focus` selectors - removes keyboard focus ring |
37
- | `neighbor/no-forced-colors-none` | `forced-color-adjust: none` inside `@media (forced-colors)` - opts out of Windows High Contrast Mode |
38
42
 
39
43
  ---
40
44
 
41
- ## Content rules - summary
45
+ ## Content rules: summary
42
46
 
43
- ESLint rules that flag accessibility and inclusion problems in string literals and JSX text - ableist language, disability metaphors, English idioms, vague link and button text, directional references, unexplained abbreviations, ALL CAPS prose, and vague error messages. All ship as `warn`. Full reference → [RULES-CONTENT.md](RULES-CONTENT.md)
47
+ ESLint rules that find problems in your text. They check for ableist language, hard-to-understand English idioms, confusing links, and unexplained short words. All of these rules are set to `warn`. Full list → [RULES-CONTENT.md](RULES-CONTENT.md)
44
48
 
45
- | Rule | What it flags | WCAG SC |
49
+ | Rule | What it finds | WCAG SC |
46
50
  | --- | --- | --- |
47
- | `no-ableist-language` | Slurs, suffering-framing, condescending euphemisms ("wheelchair-bound", "suffers from", "special needs") | 3.1.1 |
48
- | `no-disability-metaphor` | Disability used figuratively ("blind spot", "tone deaf", "paralyzed by") | - |
49
- | `no-english-idiom` | Idioms and sports metaphors opaque to ESL readers ("slam dunk", "boil the ocean", "circle back") | 3.1.5 |
50
- | `no-vague-cta` | Vague link/button text ("click here", "read more", "here") | 2.4.4 |
51
- | `no-directional-language` | Position-based instructions ("see above", "in the right sidebar") | 1.3.3 |
52
- | `no-unexplained-abbreviation` | Acronyms used without prior expansion in the file | 3.1.4 |
53
- | `no-all-caps-prose` | ALL CAPS words that screen readers may spell out letter-by-letter | - |
54
- | `no-vague-error-message` | Error messages that don't say what went wrong ("An error occurred") | 3.3.1 |
55
- | `no-ampersand-in-prose` | `&` in place of "and" - announced inconsistently by screen readers | - |
51
+ | `no-ableist-language` | Offensive words about disability or framing disability as suffering ("wheelchair-bound", "suffers from", "special needs") | 3.1.1 |
52
+ | `no-all-caps-prose` | ALL CAPS words (screen readers might read them one letter at a time) | - |
53
+ | `no-ampersand-in-prose` | Using `&` instead of "and" (screen readers read this differently) | - |
54
+ | `no-anti-lgbtq-language` | Old or offensive words about sexual orientation and gender | - |
55
+ | `no-colonial-and-violent-language` | Words based on colonialism or violence (stakeholder, target population, tackle) | - |
56
+ | `no-deficit-language` | Words that reduce people to their bad situations (the homeless, inmate, addict) | - |
57
+ | `no-device-specific-action` | Words that only make sense on a desktop computer ("click here", "press enter") | - |
58
+ | `no-directional-language` | Instructions based on where things are on the screen ("see above", "in the right sidebar") | 1.3.3 |
59
+ | `no-disability-metaphor` | Using disability as a metaphor ("blind spot", "tone deaf", "paralyzed by") | - |
60
+ | `no-english-idiom` | Phrases or sports metaphors that are hard for non-native English speakers to understand ("slam dunk", "boil the ocean", "circle back") | 3.1.5 |
61
+ | `no-exclusive-language` | Tech jargon and culturally insensitive words (blacklist, master/slave, spirit animal) | - |
62
+ | `no-gendered-language` | Gendered pronouns when the gender is unknown (he/she, his or her, mum and dad) | - |
63
+ | `no-unexplained-abbreviation` | Short words or acronyms used before you explain what they mean | 3.1.4 |
64
+ | `no-vague-cta` | Confusing link or button text ("click here", "read more", "here") | 2.4.4 |
65
+ | `no-vague-error-message` | Error messages that do not explain what is wrong ("An error occurred") | 3.3.1 |
66
+
67
+ ---
68
+
69
+ ## Native Mobile: summary
70
+
71
+ `neighbor` includes strict rule implementations translated directly from our core web libraries for native iOS (SwiftUI) and Android (Jetpack Compose). They flag issues like missing Roles, unscaled text, broken touch targets, and improper semantics directly inside Xcode and Android Studio.
72
+
73
+ - **[iOS Rules](apps/ios-app/README.md):** 8 Custom SwiftLint Rules for SwiftUI (via `.swiftlint.yml`).
74
+ - **[Android Rules](apps/android-app/README.md):** 17 Custom Android Lint Rules for Jetpack Compose (via standard Gradle module).