@a11yfred/neighbor 0.2.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,83 @@
1
1
  # Changelog
2
2
 
3
- ## 0.2.02026-05-12
3
+ ## 1.0.0 - 2026-05-12
4
+
5
+ ### Breaking change
6
+
7
+ CSS rules renamed from `ulam/` to `neighbor/` namespace:
8
+
9
+ | Old | New |
10
+ | --- | --- |
11
+ | `ulam/user-preferences` | `neighbor/user-preferences` |
12
+ | `ulam/no-outline-none` | `neighbor/no-outline-none` |
13
+ | `ulam/no-forced-colors-none` | `neighbor/no-forced-colors-none` |
14
+
15
+ Update your `.stylelintrc.json` to use the new names.
16
+
17
+ ---
18
+
19
+ ## 0.4.0 - 2026-05-12
20
+
21
+ ### New entry point
22
+
23
+ `@a11yfred/neighbor/content` - an ESLint plugin for accessibility and inclusion problems in web and app copy. Lints string literals and JSX text in JS/TS/JSX/TSX files.
24
+
25
+ ### New content rules (all `warn`)
26
+
27
+ | Rule | What it flags |
28
+ | --- | --- |
29
+ | `no-ableist-language` | Slurs, suffering-framing, and condescending euphemisms when writing about disability ("wheelchair-bound", "suffers from", "special needs", "differently abled") |
30
+ | `no-disability-metaphor` | Figurative uses of disability language ("blind spot", "tone deaf", "paralyzed by", "crippling debt") |
31
+ | `no-english-idiom` | English idioms and sports metaphors opaque to ESL and international readers ("slam dunk", "boil the ocean", "circle back", "touch base") |
32
+ | `no-vague-cta` | Vague link and button text ("click here", "read more", "here", "learn more") |
33
+ | `no-directional-language` | Layout-dependent position instructions ("see above", "in the right sidebar", "as shown below") |
34
+ | `no-unexplained-abbreviation` | Acronyms used without a prior expansion in the same file |
35
+ | `no-all-caps-prose` | ALL CAPS words that screen readers may spell out letter-by-letter |
36
+ | `no-vague-error-message` | Error messages that don't explain what went wrong ("An error occurred", "Something went wrong") |
37
+ | `no-ampersand-in-prose` | `&` in place of "and" in prose - announced inconsistently across AT vendors |
38
+
39
+ Rules are synthesised from 17 sources spanning W3C WAI, government plain language guides (US, UK, Australia, Canada), and disability language authorities (NCDJ, AP Stylebook, ADA National Network, APA Style, SIGACCESS). See [RULES-CONTENT.md](RULES-CONTENT.md) for full methodology and source citations.
40
+
41
+ ### New rule reference pages
42
+
43
+ RULES.md is now an index. Full references split into:
44
+
45
+ - [RULES-MARKUP.md](RULES-MARKUP.md) - ESLint markup rules
46
+ - [RULES-CSS.md](RULES-CSS.md) - Stylelint CSS rules
47
+ - [RULES-CONTENT.md](RULES-CONTENT.md) - content rules with sources and methodology
48
+
49
+ ### Entry point table update
50
+
51
+ `@a11yfred/neighbor/stylelint` added as an explicit stylelint alias alongside the default export.
52
+
53
+ ---
54
+
55
+ ## 0.3.0 - 2026-05-12
56
+
57
+ ### New rule
58
+
59
+ | Rule | What it catches |
60
+ | --- | --- |
61
+ | `neighbor/no-forced-colors-none` | `forced-color-adjust: none` inside `@media (forced-colors)` - actively opts out of Windows High Contrast Mode |
62
+
63
+ ### Severity changes
64
+
65
+ 10 rules moved from `warn` to `off` in the recommended config - they flag real problems but are too noisy for most codebases by default. All remain available to opt in individually:
66
+
67
+ `no-application-role`, `no-grid-role`, `no-aria-roledescription`, `no-aria-readonly`, `no-tab-without-controls`, `no-href-hash`, `warn-role-alert`, `prefer-aria-disabled`, `no-target-blank-without-label`, `no-dialog-without-close`
68
+
69
+ `no-tooltip-role-misuse` and `no-menu-role-on-nav` remain on as warns.
70
+
71
+ ### Docs
72
+
73
+ - WCAG SC and HTML spec links added throughout README and RULES.md
74
+ - CONTRIBUTING.md, PR template, and issue templates added
75
+ - README table of contents added
76
+ - @ulam described as a JavaScript framework (not React-based)
77
+
78
+ ---
79
+
80
+ ## 0.2.0 - 2026-05-12
4
81
 
5
82
  ### New rules
6
83
 
@@ -15,7 +92,7 @@ All four rules run on React, Vue, and Angular.
15
92
 
16
93
  ### Extended rules
17
94
 
18
- **`no-announce-in-render`** now runs in the Vue and Angular plugins, not just React. Safe contexts are tuned per frameworkVue recognises `onMounted`, `watch`, `watchEffect`, `nextTick`; Angular recognises `ngOnInit`, `ngAfterViewInit`, `ngOnChanges`, and class method event handlers.
95
+ **`no-announce-in-render`** now runs in the Vue and Angular plugins, not just React. Safe contexts are tuned per framework - Vue recognises `onMounted`, `watch`, `watchEffect`, `nextTick`; Angular recognises `ngOnInit`, `ngAfterViewInit`, `ngOnChanges`, and class method event handlers.
19
96
 
20
97
  ### Setup improvements
21
98
 
@@ -23,6 +100,6 @@ README now includes correct parser snippets for Vue and Angular, and separate se
23
100
 
24
101
  ---
25
102
 
26
- ## 0.1.02026-04-30
103
+ ## 0.1.0 - 2026-04-30
27
104
 
28
105
  Initial release.
@@ -0,0 +1,115 @@
1
+ # Contributing to @a11yfred/neighbor
2
+
3
+ Neighbor is maintained by [@a11yfred](https://github.com/a11yfred). Contributions are welcome from the accessibility community - practitioners, AT users, spec readers, and people who have found a gap in existing tooling.
4
+
5
+ ## What belongs here
6
+
7
+ A rule belongs in neighbor if it meets all three criteria:
8
+
9
+ 1. **Statically detectable** - the violation can be identified from markup/code alone, without a browser or AT. Runtime-only failures (color contrast, focus order in the DOM) belong in axe-core.
10
+ 2. **Not already covered** - jsx-a11y, vuejs-accessibility, @angular-eslint/template, or axe-core doesn't already flag it in a recommended config.
11
+ 3. **Expert-backed** - there's a WCAG SC, ARIA spec citation, or clear consensus from accessibility practitioners (Roselli, O'Hara, Lauke, Sutton, Pickering, Groves, Eggert, etc.).
12
+
13
+ If you're unsure, open an issue before writing a rule. A brief description and a source is enough to start a conversation.
14
+
15
+ ## What doesn't belong here
16
+
17
+ - Rules that require runtime information (computed styles, DOM layout, AT output)
18
+ - Rules already in jsx-a11y recommended - neighbor extends it, not replaces it
19
+ - Opinionated style rules without a clear accessibility impact
20
+ - Rules with very high false-positive rates on real codebases (see the rejected rules list in [RULES.md](RULES.md))
21
+
22
+ ## Setup
23
+
24
+ ```bash
25
+ git clone https://github.com/a11yfred/neighbor.git
26
+ cd neighbor
27
+ npm install
28
+ ```
29
+
30
+ No build step. Rules are plain ES modules - edit and run ESLint directly.
31
+
32
+ ## How rules are structured
33
+
34
+ All ESLint rules live in [`lib/rules.js`](lib/rules.js) as factory functions:
35
+
36
+ ```js
37
+ export function makeMyNewRule(h) {
38
+ return {
39
+ meta: {
40
+ type: 'problem', // 'problem' | 'suggestion'
41
+ docs: { description: '...' },
42
+ messages: { myMessage: '...' },
43
+ schema: [],
44
+ },
45
+ create(context) {
46
+ return {
47
+ [h.elementVisitor](node) {
48
+ // h adapts the rule to React/Vue/Angular ASTs
49
+ },
50
+ }
51
+ },
52
+ }
53
+ }
54
+ ```
55
+
56
+ The `h` adapter gives you a uniform interface across all three frameworks:
57
+
58
+ | Helper | Returns |
59
+ |---|---|
60
+ | `h.getAttr(node, name)` | attribute node or `null` |
61
+ | `h.getAttrStringValue(attr)` | string or `null` (null for dynamic expressions) |
62
+ | `h.getElementName(node)` | lowercase tag name, or `null` for custom components |
63
+ | `h.hasAttr(node, name)` | boolean |
64
+ | `h.getRoleValue(node)` | role string or `null` |
65
+ | `h.hasAccessibleName(node)` | boolean - checks `aria-label` / `aria-labelledby` |
66
+ | `h.isInteractiveElement(node)` | boolean |
67
+ | `h.getParent(node)` | parent element node or `null` |
68
+ | `h.getAncestors(node)` | iterable of ancestor element nodes, root-ward |
69
+ | `h.getChildOpeningElements(node)` | iterable of direct child element nodes |
70
+ | `h.getInnerHtmlAttr(node)` | `dangerouslySetInnerHTML` / `v-html` / `[innerHTML]` node or `null` |
71
+ | `h.elementVisitor` | AST node type string for `create()` visitor key |
72
+ | `h.elementWithChildrenVisitor` | visitor key for rules that need child access |
73
+
74
+ **Angular caveat:** `getParent()` and `getAncestors()` return `null`/nothing for Angular - the template parser doesn't attach parent pointers. Rules that require ancestor walking should degrade gracefully (skip the check, don't throw).
75
+
76
+ After writing your factory:
77
+
78
+ 1. Add it to `RULE_FACTORIES` at the bottom of `lib/rules.js`
79
+ 2. Add it to `buildRecommendedRules()` at the appropriate severity (`'error'` / `'warn'` / `'off'`)
80
+ 3. If it's Vue/Angular-only (porting a jsx-a11y gap), add it to `buildPortabilityRules()` instead
81
+
82
+ Stylelint rules live in [`neighbor-stylelint.mjs`](neighbor-stylelint.mjs) and use the PostCSS AST directly. See the existing rules for the pattern.
83
+
84
+ ## Severity guidance
85
+
86
+ | Severity | When to use |
87
+ |---|---|
88
+ | `error` | Unambiguous AT breakage - a phantom control, broken name computation, HTML spec violation. No legitimate override. |
89
+ | `warn` | Strong guidance with a clear accessibility basis, but real codebases occasionally have justified exceptions. |
90
+ | `off` | Real problem, but fires too often on legitimate patterns to be on by default. Make it available; let teams opt in. |
91
+
92
+ When in doubt, start at `warn`. It's easier to promote a rule to `error` than to demote it after people have already configured it.
93
+
94
+ ## Commit style
95
+
96
+ ```
97
+ feat: add no-my-new-rule (short description)
98
+ fix: correct false positive in no-existing-rule
99
+ docs: update RULES.md for no-my-new-rule
100
+ ```
101
+
102
+ No ticket numbers required.
103
+
104
+ ## Opening a PR
105
+
106
+ Use the PR template. The key things:
107
+
108
+ - **What problem does this flag?** Link a WCAG SC, ARIA spec section, or expert source.
109
+ - **Why can't axe-core catch it at runtime instead?** (If it can, it probably belongs there.)
110
+ - **What are the false-positive cases?** Be honest - we'd rather move a rule to `off` than reject it.
111
+ - **Does it degrade gracefully for Angular?** (Parent walking unavailable.)
112
+
113
+ ## Questions
114
+
115
+ Open an issue or reach out in the [Web A11y Slack](https://web-a11y.slack.com). The `#tools` channel is a good place to discuss rule ideas before writing code.
package/README.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  Neighbor is an accessibility linting plugin for ESLint and Stylelint that builds on jsx-a11y. It looks to cover gaps: bad ARIA patterns, live region misuse, missing names on roles, and CSS that removes focus indicators. It also brings that coverage to Vue and Angular, where jsx-a11y does not apply.
4
4
 
5
+ Some rules are specific to **@ulam** - an upcoming JavaScript framework by the same author. Those rules are prefixed `no-announce-in-render`, `no-hash-router-in-remix`, and `no-use-page-title-in-remix`. They activate only when @ulam-related imports are detected and are harmless in non-@ulam projects.
6
+
7
+ ## Contents
8
+
9
+ - [Install](#install)
10
+ - [Entry points](#entry-points)
11
+ - [Setup](#setup)
12
+ - [React / JSX](#react--jsx)
13
+ - [Remix 2](#remix-2)
14
+ - [Remix 3](#remix-3)
15
+ - [Vue](#vue)
16
+ - [Angular](#angular)
17
+ - [Stylelint](#stylelint)
18
+ - [Content linting](#content-linting)
19
+ - [Peer dependencies](#peer-dependencies)
20
+ - [What neighbor adds](#what-neighbor-adds)
21
+ - [ESLint - React / JSX](#eslint--react--jsx)
22
+ - [ESLint - Remix 2](#eslint--remix-2)
23
+ - [ESLint - Vue SFCs](#eslint--vue-sfcs)
24
+ - [ESLint - Angular templates](#eslint--angular-templates)
25
+ - [Stylelint - CSS](#stylelint--css)
26
+ - [Content linter](#content-linter)
27
+ - [Rule severity](#rule-severity)
28
+ - [Contributing](CONTRIBUTING.md)
29
+ - [See also](#see-also)
30
+ - [License](#license)
31
+
5
32
  ## Install
6
33
 
7
34
  ```bash
@@ -12,10 +39,12 @@ npm install --save-dev @a11yfred/neighbor
12
39
 
13
40
  | Import | Use for |
14
41
  | --- | --- |
15
- | `@a11yfred/neighbor/eslint` | React / JSX, Remix 2 |
16
- | `@a11yfred/neighbor/eslint-vue` | Vue SFCs |
17
- | `@a11yfred/neighbor/eslint-angular` | Angular templates |
18
- | `@a11yfred/neighbor` | Stylelint CSS user-preference fallbacks |
42
+ | `@a11yfred/neighbor/eslint` | React / JSX, Remix 2 - markup rules |
43
+ | `@a11yfred/neighbor/eslint-vue` | Vue SFCs - markup rules |
44
+ | `@a11yfred/neighbor/eslint-angular` | Angular templates - markup rules |
45
+ | `@a11yfred/neighbor/content` | Any JS/TS/JSX/TSX - content and prose rules |
46
+ | `@a11yfred/neighbor` | Stylelint - CSS rules (default export) |
47
+ | `@a11yfred/neighbor/stylelint` | Stylelint - CSS rules (explicit) |
19
48
 
20
49
  ## Setup
21
50
 
@@ -62,7 +91,7 @@ export default [
62
91
 
63
92
  ### Remix 3
64
93
 
65
- Remix 3 is framework-agnostic and does not require React. Neighbor does not have a dedicated Remix 3 entry pointuse the entry point that matches your renderer.
94
+ Remix 3 is framework-agnostic and does not require React. Neighbor does not have a dedicated Remix 3 entry point - use the entry point that matches your renderer.
66
95
 
67
96
  If you are using React with Remix 3:
68
97
 
@@ -134,23 +163,67 @@ export default [
134
163
 
135
164
  ### Stylelint
136
165
 
137
- ```js
166
+ ```json
138
167
  // .stylelintrc.json
139
168
  {
140
169
  "plugins": ["@a11yfred/neighbor"],
141
170
  "rules": {
142
- "ulam/user-preferences": true,
143
- "ulam/no-outline-none": true
171
+ "neighbor/user-preferences": true,
172
+ "neighbor/no-outline-none": true,
173
+ "neighbor/no-forced-colors-none": true
144
174
  }
145
175
  }
146
176
  ```
147
177
 
178
+ ### Content linting
179
+
180
+ The content plugin lints string literals and JSX text in JavaScript, TypeScript, JSX, and TSX files. It is separate from the markup plugins and can be used alongside any of them.
181
+
182
+ ```bash
183
+ npm install --save-dev @a11yfred/neighbor
184
+ ```
185
+
186
+ ```js
187
+ // eslint.config.js
188
+ import neighborContent from '@a11yfred/neighbor/content'
189
+
190
+ export default [
191
+ {
192
+ files: ['**/*.{js,jsx,ts,tsx}'],
193
+ plugins: { ...neighborContent.configs.recommended.plugins },
194
+ rules: { ...neighborContent.configs.recommended.rules },
195
+ },
196
+ ]
197
+ ```
198
+
199
+ To use alongside the React markup plugin:
200
+
201
+ ```js
202
+ // eslint.config.js
203
+ import neighbor from '@a11yfred/neighbor/eslint'
204
+ import neighborContent from '@a11yfred/neighbor/content'
205
+
206
+ export default [
207
+ {
208
+ files: ['**/*.{js,jsx,ts,tsx}'],
209
+ plugins: {
210
+ ...neighbor.configs.recommended.plugins,
211
+ ...neighborContent.configs.recommended.plugins,
212
+ },
213
+ rules: {
214
+ ...neighbor.configs.recommended.rules,
215
+ ...neighborContent.configs.recommended.rules,
216
+ },
217
+ },
218
+ ]
219
+ ```
220
+
148
221
  ## Peer dependencies
149
222
 
150
223
  | Peer | Required for |
151
224
  | --- | --- |
152
225
  | `eslint >= 8` | Any ESLint entry point |
153
- | `eslint-plugin-jsx-a11y >= 6` | React configneighbor extends it, not replaces it |
226
+ | `eslint-plugin-jsx-a11y >= 6` | React config - neighbor extends it, not replaces it |
154
227
  | `eslint-plugin-vuejs-accessibility >= 2` | Vue config |
155
228
  | `@angular-eslint/eslint-plugin-template >= 17` | Angular config |
156
229
  | `stylelint >= 14` | Stylelint config |
@@ -159,61 +232,61 @@ All peers are optional. Install only what your project uses.
159
232
 
160
233
  ## What neighbor adds
161
234
 
162
- ### ESLintReact / JSX
235
+ ### ESLint - React / JSX
163
236
 
164
237
  Base: `eslint-plugin-jsx-a11y`
165
238
 
166
239
  | What it checks | Rule | WCAG SC |
167
240
  | --- | --- | --- |
168
- | `aria-disabled` keeps element reachable | `prefer-aria-disabled` | 2.1.1 |
169
- | `aria-disabled` must block click handler | `no-unblocked-aria-disabled` | 2.1.1 |
170
- | `aria-label` on a generic element with no role | `no-aria-label-on-generic` | 1.3.1 |
171
- | `role="alert"` overuse | `warn-role-alert` | 4.1.3 |
172
- | `aria-live="assertive"` outside `role="alert"` | `no-assertive-live-overuse` | 4.1.3 |
173
- | `role="dialog"` requires accessible name | `no-roles-without-name` | 4.1.2 |
174
- | `role="group"` with form controls requires name | `no-group-without-name` | 1.3.1 |
175
- | `role="tooltip"` requires `id` on the tooltip | `no-tooltip-role-misuse` | 4.1.2 |
176
- | `role="application"` disables AT browse mode | `no-application-role` | |
177
- | `role="grid"` almost always wrong | `no-grid-role` | |
178
- | `role="menu"` on nav triggers wrong AT mode | `no-menu-role-on-nav` | 2.1.1 |
179
- | `role="presentation"` on a focusable element | `no-presentation-on-focusable` | 2.1.1 |
180
- | `role="log"` must not contain interactive children | `no-log-with-interactive-children` | 4.1.2 |
181
- | `role="img"` requires accessible name | `no-image-role-without-name` | 4.1.2 |
182
- | `role="tab"` requires `aria-selected` | `no-tabs-without-structure` | 4.1.2 |
183
- | `role="tab"` should declare `aria-controls` | `no-tab-without-controls` | 4.1.2 |
184
- | `role="combobox"` requires `aria-expanded` | `no-combobox-without-expanded` | 4.1.2 |
185
- | `role="slider"` requires value range attributes | `no-slider-without-range` | 4.1.2 |
186
- | `role="spinbutton"` requires value range attributes | `no-spinbutton-without-range` | 4.1.2 |
187
- | `role="listbox"` requires `role="option"` children | `no-listbox-without-option` | 4.1.2 |
188
- | `role="tree"` requires `role="treeitem"` children | `no-tree-without-treeitem` | 4.1.2 |
189
- | `role="feed"` requires `role="article"` children | `no-feed-without-article` | 4.1.2 |
190
- | `aria-hidden="true"` + `role="none"` is redundant | `no-redundant-aria-hidden-with-presentation` | |
191
- | `aria-roledescription` does not translate | `no-aria-roledescription` | |
192
- | `aria-readonly` has poor AT support | `no-aria-readonly` | |
193
- | `aria-owns` on a void element | `no-aria-owns-on-void` | 4.1.2 |
194
- | `aria-activedescendant` requires a non-empty static ID | `no-aria-activedescendant-without-id` | 4.1.2 |
195
- | `aria-required` only valid on form-control roles | `no-aria-required-on-non-form` | 4.1.2 |
196
- | `<a>` with only aria-hidden children | `no-aria-hidden-in-link` | 4.1.2 |
197
- | `<button>` with only aria-hidden children | `no-empty-button` | 4.1.2 |
198
- | `<input>` placeholder used as sole label | `no-placeholder-only` | 1.3.1 |
199
- | `<input>` with invalid type value | `no-input-type-invalid` | 1.3.5 |
200
- | `<button>` in a form missing explicit type | `no-button-type-missing` | HTML spec |
201
- | `<summary>` outside `<details>` | `no-summary-without-details` | 2.1.1 |
202
- | `<a href="#">` used as a button | `no-href-hash` | 2.1.1 |
203
- | `target="_blank"` without new-tab disclosure | `no-target-blank-without-label` | 3.2.2 |
204
- | Duplicate `id` breaks ARIA relationships | `no-duplicate-id` | 1.3.1 |
205
- | Positive `tabIndex` breaks tab order | `no-positive-tabindex` | 2.4.3 |
206
- | Heading inside an interactive element | `no-heading-inside-interactive` | 4.1.2 |
207
- | `title` attribute as the only accessible name | `no-title-as-label` | 2.4.6 |
208
- | `<video>` or `<audio autoplay>` without controls | `no-autoplay-without-controls` | 1.4.2 |
209
- | Mouse-only events without keyboard equivalents | `no-mouse-only-events` | 2.1.1 |
210
- | `aria-labelledby`/`describedby` references missing `id` | `no-labelledby-missing-target` | 4.1.2 |
211
- | `dangerouslySetInnerHTML` outside a live region | `no-dynamic-content-without-live` | 4.1.3 |
212
- | Multiple `<label>` elements for the same control | `form-field-multiple-labels` | 1.3.1 |
213
- | `<th>` or header role with no accessible text | `no-empty-table-header` | 1.3.1 |
214
- | `announce()` called in component render body | `no-announce-in-render` | 4.1.3 |
215
-
216
- ### ESLintRemix 2
241
+ | `aria-disabled` keeps element reachable | `prefer-aria-disabled` | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
242
+ | `aria-disabled` must block click handler | `no-unblocked-aria-disabled` | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
243
+ | `aria-label` on a generic element with no role | `no-aria-label-on-generic` | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
244
+ | `role="alert"` overuse | `warn-role-alert` | [4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
245
+ | `aria-live="assertive"` outside `role="alert"` | `no-assertive-live-overuse` | [4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
246
+ | `role="dialog"` requires accessible name | `no-roles-without-name` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
247
+ | `role="group"` with form controls requires name | `no-group-without-name` | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
248
+ | `role="tooltip"` requires `id` on the tooltip | `no-tooltip-role-misuse` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
249
+ | `role="application"` disables AT browse mode | `no-application-role` | - |
250
+ | `role="grid"` almost always wrong | `no-grid-role` | - |
251
+ | `role="menu"` on nav triggers wrong AT mode | `no-menu-role-on-nav` | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
252
+ | `role="presentation"` on a focusable element | `no-presentation-on-focusable` | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
253
+ | `role="log"` must not contain interactive children | `no-log-with-interactive-children` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
254
+ | `role="img"` requires accessible name | `no-image-role-without-name` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
255
+ | `role="tab"` requires `aria-selected` | `no-tabs-without-structure` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
256
+ | `role="tab"` should declare `aria-controls` | `no-tab-without-controls` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
257
+ | `role="combobox"` requires `aria-expanded` | `no-combobox-without-expanded` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
258
+ | `role="slider"` requires value range attributes | `no-slider-without-range` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
259
+ | `role="spinbutton"` requires value range attributes | `no-spinbutton-without-range` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
260
+ | `role="listbox"` requires `role="option"` children | `no-listbox-without-option` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
261
+ | `role="tree"` requires `role="treeitem"` children | `no-tree-without-treeitem` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
262
+ | `role="feed"` requires `role="article"` children | `no-feed-without-article` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
263
+ | `aria-hidden="true"` + `role="none"` is redundant | `no-redundant-aria-hidden-with-presentation` | - |
264
+ | `aria-roledescription` does not translate | `no-aria-roledescription` | - |
265
+ | `aria-readonly` has poor AT support | `no-aria-readonly` | - |
266
+ | `aria-owns` on a void element | `no-aria-owns-on-void` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
267
+ | `aria-activedescendant` requires a non-empty static ID | `no-aria-activedescendant-without-id` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
268
+ | `aria-required` only valid on form-control roles | `no-aria-required-on-non-form` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
269
+ | `<a>` with only aria-hidden children | `no-aria-hidden-in-link` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
270
+ | `<button>` with only aria-hidden children | `no-empty-button` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
271
+ | `<input>` placeholder used as sole label | `no-placeholder-only` | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
272
+ | `<input>` with invalid type value | `no-input-type-invalid` | [1.3.5](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose) |
273
+ | `<button>` in a form missing explicit type | `no-button-type-missing` | [HTML spec](https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element) |
274
+ | `<summary>` outside `<details>` | `no-summary-without-details` | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
275
+ | `<a href="#">` used as a button | `no-href-hash` | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
276
+ | `target="_blank"` without new-tab disclosure | `no-target-blank-without-label` | [3.2.2](https://www.w3.org/WAI/WCAG21/Understanding/on-input) |
277
+ | Duplicate `id` breaks ARIA relationships | `no-duplicate-id` | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
278
+ | Positive `tabIndex` breaks tab order | `no-positive-tabindex` | [2.4.3](https://www.w3.org/WAI/WCAG21/Understanding/focus-order) |
279
+ | Heading inside an interactive element | `no-heading-inside-interactive` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
280
+ | `title` attribute as the only accessible name | `no-title-as-label` | [2.4.6](https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels) |
281
+ | `<video>` or `<audio autoplay>` without controls | `no-autoplay-without-controls` | [1.4.2](https://www.w3.org/WAI/WCAG21/Understanding/audio-control) |
282
+ | Mouse-only events without keyboard equivalents | `no-mouse-only-events` | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
283
+ | `aria-labelledby`/`describedby` references missing `id` | `no-labelledby-missing-target` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
284
+ | `dangerouslySetInnerHTML` outside a live region | `no-dynamic-content-without-live` | [4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
285
+ | Multiple `<label>` elements for the same control | `form-field-multiple-labels` | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
286
+ | `<th>` or header role with no accessible text | `no-empty-table-header` | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
287
+ | `announce()` called in component render body | `no-announce-in-render` | [4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
288
+
289
+ ### ESLint - Remix 2
217
290
 
218
291
  Same as React / JSX. Additional rules activate when Remix imports are detected in the file being linted:
219
292
 
@@ -222,7 +295,7 @@ Same as React / JSX. Additional rules activate when Remix imports are detected i
222
295
  | `@ulam` hash router alongside `react-router` | `no-hash-router-in-remix` | warn |
223
296
  | `usePageTitle()` alongside `react-router` | `no-use-page-title-in-remix` | warn |
224
297
 
225
- ### ESLintVue SFCs
298
+ ### ESLint - Vue SFCs
226
299
 
227
300
  Base: `eslint-plugin-vuejs-accessibility`
228
301
 
@@ -230,31 +303,50 @@ Neighbor adds everything in the React table above, adapted for Vue's AST (`v-htm
230
303
 
231
304
  | What it checks | Rule | WCAG SC |
232
305
  | --- | --- | --- |
233
- | Ambiguous link text ("click here", "read more") | `no-anchor-ambiguous-text` | 2.4.4 |
234
- | `<a>` with no content and no accessible name | `no-anchor-no-content` | 4.1.2 |
235
- | Invalid ARIA attribute values | `no-invalid-aria-prop-value` | 4.1.2 |
236
- | Invalid `autocomplete` token | `no-autocomplete-invalid` | 1.3.5 |
237
- | Heading with no content | `no-heading-no-content` | 2.4.6 |
238
- | `<iframe>` without `title` | `no-iframe-no-title` | 4.1.2 |
239
- | Alt text contains "image", "photo" | `no-img-redundant-alt` | 1.1.1 |
240
- | `accessKey` attribute | `no-access-key` | 2.1.4 |
241
- | `scope` on `<td>` (only valid on `<th>`) | `no-scope-on-td` | 1.3.1 |
242
- | `announce()` called outside `onMounted`/`watch`/handler | `no-announce-in-render` | 4.1.3 |
243
-
244
- ### ESLintAngular templates
306
+ | Ambiguous link text ("click here", "read more") | `no-anchor-ambiguous-text` | [2.4.4](https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context) |
307
+ | `<a>` with no content and no accessible name | `no-anchor-no-content` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
308
+ | Invalid ARIA attribute values | `no-invalid-aria-prop-value` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
309
+ | Invalid `autocomplete` token | `no-autocomplete-invalid` | [1.3.5](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose) |
310
+ | Heading with no content | `no-heading-no-content` | [2.4.6](https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels) |
311
+ | `<iframe>` without `title` | `no-iframe-no-title` | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
312
+ | Alt text contains "image", "photo" | `no-img-redundant-alt` | [1.1.1](https://www.w3.org/WAI/WCAG21/Understanding/non-text-content) |
313
+ | `accessKey` attribute | `no-access-key` | [2.1.4](https://www.w3.org/WAI/WCAG21/Understanding/character-key-shortcuts) |
314
+ | `scope` on `<td>` (only valid on `<th>`) | `no-scope-on-td` | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
315
+ | `announce()` called outside `onMounted`/`watch`/handler | `no-announce-in-render` | [4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
316
+
317
+ ### ESLint - Angular templates
245
318
 
246
319
  Base: `@angular-eslint/eslint-plugin-template`
247
320
 
248
- Neighbor adds the same rule set as Vue, adapted for Angular's template AST (`[innerHTML]` instead of `dangerouslySetInnerHTML`). The `no-announce-in-render` rule also lints Angular component TypeScript filessee the setup instructions for how to configure it for `.ts` files alongside `.html` templates.
321
+ Neighbor adds the same rule set as Vue, adapted for Angular's template AST (`[innerHTML]` instead of `dangerouslySetInnerHTML`). The `no-announce-in-render` rule also lints Angular component TypeScript files - see the setup instructions for how to configure it for `.ts` files alongside `.html` templates.
249
322
 
250
323
  **Known limitation:** Angular's template parser does not attach parent pointers to AST nodes. Rules that need to 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 for Angular (no ancestor walk).
251
324
 
252
- ### StylelintCSS
325
+ ### Stylelint - CSS
253
326
 
254
327
  | Rule | What it checks |
255
328
  | --- | --- |
256
- | `ulam/user-preferences` | Warns when motion, transparency, or alpha colors are used without `@media (prefers-*)` fallbacks |
257
- | `ulam/no-outline-none` | Disallows bare `outline: none` or `outline: 0` outside `:focus` selectors |
329
+ | `neighbor/user-preferences` | Warns when motion, transparency, or alpha colors are used without `@media (prefers-*)` fallbacks - [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) |
330
+ | `neighbor/no-outline-none` | Disallows bare `outline: none` or `outline: 0` outside `:focus` selectors - [SC 2.4.7](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible) |
331
+ | `neighbor/no-forced-colors-none` | Disallows `forced-color-adjust: none` inside `@media (forced-colors)` - opts out of Windows High Contrast Mode - [SC 1.4.11](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast) |
332
+
333
+ ### Content linter
334
+
335
+ Rules that flag accessibility and inclusion problems in web and app copy. Works on string literals and JSX text in JS/TS/JSX/TSX files.
336
+
337
+ | Rule | What it flags | Severity | WCAG SC |
338
+ | --- | --- | --- | --- |
339
+ | `no-ableist-language` | Slurs, condescending euphemisms, suffering-framing ("suffers from", "wheelchair-bound", "special needs") | warn | [3.1.1](https://www.w3.org/WAI/WCAG22/Understanding/language-of-page) |
340
+ | `no-disability-metaphor` | Figurative use of disability language ("blind spot", "tone deaf", "paralyzed by") | warn | - |
341
+ | `no-english-idiom` | Idioms and sports metaphors opaque to ESL readers ("ball park", "slam dunk", "boil the ocean") | warn | [3.1.5](https://www.w3.org/WAI/WCAG22/Understanding/reading-level) |
342
+ | `no-vague-cta` | Vague link and button text ("click here", "read more", "here") | warn | [2.4.4](https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context) |
343
+ | `no-directional-language` | Layout-dependent position references ("see above", "in the right sidebar") | warn | [1.3.3](https://www.w3.org/WAI/WCAG22/Understanding/sensory-characteristics) |
344
+ | `no-unexplained-abbreviation` | Acronyms used without a prior expansion in the same file | warn | [3.1.4](https://www.w3.org/WAI/WCAG22/Understanding/abbreviations) |
345
+ | `no-all-caps-prose` | ALL CAPS words in prose that screen readers may spell out letter-by-letter | warn | - |
346
+ | `no-vague-error-message` | Error messages that don't explain what went wrong ("An error occurred", "Something went wrong") | warn | [3.3.1](https://www.w3.org/WAI/WCAG22/Understanding/error-identification) |
347
+ | `no-ampersand-in-prose` | `&` used in place of "and" in prose - announced inconsistently by screen readers | warn | - |
348
+
349
+ See [RULES-CONTENT.md](RULES-CONTENT.md) for the full rule reference including sources, methodology, and the language-evolution note.
258
350
 
259
351
  ## Rule severity
260
352
 
@@ -262,12 +354,17 @@ Neighbor adds the same rule set as Vue, adapted for Angular's template AST (`[in
262
354
  | --- | --- |
263
355
  | `error` | Definite AT breakage or HTML spec violation |
264
356
  | `warn` | Strong guidance, occasional legitimate overrides exist |
357
+ | `off` | Available but disabled - too noisy for most codebases, enable if it fits your project |
265
358
 
266
359
  All rules can be overridden in your config.
267
360
 
268
361
  ## See also
269
362
 
270
- - [RULES.md](RULES.md) full rule list with descriptions
363
+ - [RULES.md](RULES.md) - rule index across all domains
364
+ - [RULES-MARKUP.md](RULES-MARKUP.md) - full ESLint rule reference (markup)
365
+ - [RULES-CSS.md](RULES-CSS.md) - full Stylelint rule reference (CSS)
366
+ - [RULES-CONTENT.md](RULES-CONTENT.md) - full content rule reference with sources
367
+ - [neighbor-vale](https://github.com/a11yfred/neighbor-vale) - companion Vale package for prose linting in Markdown, MDX, and HTML
271
368
 
272
369
  ## License
273
370