@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 +80 -3
- package/CONTRIBUTING.md +115 -0
- package/README.md +174 -77
- package/RULES-CONTENT.md +296 -0
- package/RULES-CSS.md +61 -0
- package/RULES-MARKUP.md +156 -0
- package/RULES.md +55 -0
- package/lib/content-rules.js +858 -0
- package/lib/helpers-angular.js +146 -146
- package/lib/helpers-jsx.js +193 -193
- package/lib/helpers-vue.js +151 -151
- package/lib/helpers.js +37 -37
- package/lib/rules.js +2413 -2411
- package/lib/ulam-rules.js +301 -301
- package/neighbor-content.mjs +80 -0
- package/neighbor-eslint-angular.mjs +68 -68
- package/neighbor-eslint-vue.mjs +48 -48
- package/neighbor-eslint.mjs +56 -56
- package/neighbor-stylelint.mjs +257 -196
- package/package.json +18 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,83 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
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 framework
|
|
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.0
|
|
103
|
+
## 0.1.0 - 2026-04-30
|
|
27
104
|
|
|
28
105
|
Initial release.
|
package/CONTRIBUTING.md
ADDED
|
@@ -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` |
|
|
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 point
|
|
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
|
-
```
|
|
166
|
+
```json
|
|
138
167
|
// .stylelintrc.json
|
|
139
168
|
{
|
|
140
169
|
"plugins": ["@a11yfred/neighbor"],
|
|
141
170
|
"rules": {
|
|
142
|
-
"
|
|
143
|
-
"
|
|
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 config
|
|
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
|
-
### ESLint
|
|
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
|
-
### ESLint
|
|
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
|
-
### ESLint
|
|
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
|
-
### ESLint
|
|
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 files
|
|
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
|
-
### Stylelint
|
|
325
|
+
### Stylelint - CSS
|
|
253
326
|
|
|
254
327
|
| Rule | What it checks |
|
|
255
328
|
| --- | --- |
|
|
256
|
-
| `
|
|
257
|
-
| `
|
|
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)
|
|
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
|
|