@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/CHANGELOG.md +81 -90
- package/CONTRIBUTING.md +40 -40
- package/README.md +4 -472
- package/RULES-CONTENT.md +240 -81
- package/RULES-CSS.md +41 -19
- package/RULES-MARKUP.md +168 -94
- package/RULES.md +47 -28
- package/lib/content-rules.js +216 -0
- package/lib/framework-rules.js +282 -0
- package/lib/helpers-webcomponents.js +134 -0
- package/lib/rules.js +374 -3
- package/neighbor-content.mjs +1 -1
- package/neighbor-eslint-angular.mjs +29 -8
- package/neighbor-eslint-lit.mjs +85 -0
- package/neighbor-eslint-remix3.mjs +49 -0
- package/neighbor-eslint-vue.mjs +26 -6
- package/neighbor-eslint-webcomponents.mjs +41 -0
- package/neighbor-eslint.mjs +13 -6
- package/neighbor-stylelint.mjs +141 -3
- package/package.json +10 -11
package/README.md
CHANGED
|
@@ -1,475 +1,7 @@
|
|
|
1
1
|
# @a11yfred/neighbor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The a11yfred accessibility linter ecosystem core packages. This specific package houses the ESLint and Stylelint rules.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- [Install](#install)
|
|
10
|
-
- [Entry points](#entry-points)
|
|
11
|
-
- [Setup](#setup)
|
|
12
|
-
- [Vanilla JS / plain HTML (no framework)](#vanilla-js--plain-html-no-framework)
|
|
13
|
-
- [React / JSX](#react-jsx)
|
|
14
|
-
- [Remix 2](#remix-2)
|
|
15
|
-
- [Remix 3](#remix-3)
|
|
16
|
-
- [Vue](#vue)
|
|
17
|
-
- [Angular](#angular)
|
|
18
|
-
- [Stylelint](#stylelint)
|
|
19
|
-
- [Content linting](#content-linting)
|
|
20
|
-
- [Peer dependencies](#peer-dependencies)
|
|
21
|
-
- [What neighbor adds](#what-neighbor-adds)
|
|
22
|
-
- [ESLint - React / JSX](#eslint-react-jsx)
|
|
23
|
-
- [ESLint - Remix 2](#eslint-remix-2)
|
|
24
|
-
- [ESLint - Vue SFCs](#eslint-vue-sfcs)
|
|
25
|
-
- [ESLint - Angular templates](#eslint-angular-templates)
|
|
26
|
-
- [Stylelint - CSS](#stylelint-css)
|
|
27
|
-
- [Content linter](#content-linter)
|
|
28
|
-
- [Rule severity](#rule-severity)
|
|
29
|
-
- [Contributing](CONTRIBUTING.md)
|
|
30
|
-
- [See also](#see-also)
|
|
31
|
-
- [License](#license)
|
|
32
|
-
|
|
33
|
-
## Install
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
npm install --save-dev @a11yfred/neighbor
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Entry points
|
|
40
|
-
|
|
41
|
-
| Import | Use for |
|
|
42
|
-
| --- | --- |
|
|
43
|
-
| `@a11yfred/neighbor/eslint` | React / JSX, Remix 2 - markup rules |
|
|
44
|
-
| `@a11yfred/neighbor/eslint-vue` | Vue SFCs - markup rules |
|
|
45
|
-
| `@a11yfred/neighbor/eslint-angular` | Angular templates - markup rules |
|
|
46
|
-
| `@a11yfred/neighbor/content` | Any JS/TS/JSX/TSX - content and prose rules |
|
|
47
|
-
| `@a11yfred/neighbor` | Stylelint - CSS rules (default export) |
|
|
48
|
-
| `@a11yfred/neighbor/stylelint` | Stylelint - CSS rules (explicit) |
|
|
49
|
-
|
|
50
|
-
## Setup
|
|
51
|
-
|
|
52
|
-
### Vanilla JS / plain HTML (no framework)
|
|
53
|
-
|
|
54
|
-
If you write plain JavaScript with no JSX, React, Vue, or Angular, only the **Stylelint CSS rules** and the **content linter** apply. The ESLint markup rules require a component framework. They lint JSX or template syntax that plain JS does not have.
|
|
55
|
-
|
|
56
|
-
What you get:
|
|
57
|
-
|
|
58
|
-
| Plugin | What it checks |
|
|
59
|
-
| --- | --- |
|
|
60
|
-
| Stylelint (`@a11yfred/neighbor`) | CSS: bare `outline: none`, forced-colors opt-out, motion/transparency without `prefers-*` fallbacks |
|
|
61
|
-
| Content linter (`@a11yfred/neighbor/content`) | JS strings: ableist language, vague CTAs, unexplained abbreviations, idioms, all-caps prose |
|
|
62
|
-
|
|
63
|
-
**Stylelint setup** (CSS only, no framework needed):
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
npm install --save-dev stylelint stylelint-config-standard @a11yfred/neighbor
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
```json
|
|
70
|
-
// .stylelintrc.json
|
|
71
|
-
{
|
|
72
|
-
"extends": ["stylelint-config-standard"],
|
|
73
|
-
"plugins": ["@a11yfred/neighbor"],
|
|
74
|
-
"rules": {
|
|
75
|
-
"neighbor/no-outline-none": true,
|
|
76
|
-
"neighbor/no-forced-colors-none": true,
|
|
77
|
-
"neighbor/user-preferences": true
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Run it:
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
npx stylelint "**/*.css"
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
**Content linter setup** (plain JS string literals, no framework needed):
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
npm install --save-dev eslint @a11yfred/neighbor
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
```js
|
|
95
|
-
// eslint.config.js (ESLint flat config, ESLint >= 8)
|
|
96
|
-
import neighborContent from '@a11yfred/neighbor/content'
|
|
97
|
-
|
|
98
|
-
export default [
|
|
99
|
-
{
|
|
100
|
-
files: ['**/*.js'],
|
|
101
|
-
plugins: { ...neighborContent.configs.recommended.plugins },
|
|
102
|
-
rules: { ...neighborContent.configs.recommended.rules },
|
|
103
|
-
},
|
|
104
|
-
]
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Run it:
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
npx eslint src/
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Both together:
|
|
114
|
-
|
|
115
|
-
```js
|
|
116
|
-
// eslint.config.js
|
|
117
|
-
import neighborContent from '@a11yfred/neighbor/content'
|
|
118
|
-
|
|
119
|
-
export default [
|
|
120
|
-
{
|
|
121
|
-
files: ['**/*.js'],
|
|
122
|
-
plugins: { ...neighborContent.configs.recommended.plugins },
|
|
123
|
-
rules: { ...neighborContent.configs.recommended.rules },
|
|
124
|
-
},
|
|
125
|
-
]
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
```json
|
|
129
|
-
// .stylelintrc.json
|
|
130
|
-
{
|
|
131
|
-
"extends": ["stylelint-config-standard"],
|
|
132
|
-
"plugins": ["@a11yfred/neighbor"],
|
|
133
|
-
"rules": {
|
|
134
|
-
"neighbor/no-outline-none": true,
|
|
135
|
-
"neighbor/no-forced-colors-none": true,
|
|
136
|
-
"neighbor/user-preferences": true
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
The ESLint markup rules (`@a11yfred/neighbor/eslint` and variants) require JSX or a component template syntax. They will not produce useful output on plain HTML or vanilla JS files and should be skipped.
|
|
142
|
-
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
### React / JSX
|
|
146
|
-
|
|
147
|
-
Neighbor works alongside `eslint-plugin-jsx-a11y`. Install both.
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
npm install --save-dev eslint-plugin-jsx-a11y @a11yfred/neighbor
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
```js
|
|
154
|
-
// eslint.config.js
|
|
155
|
-
import neighbor from '@a11yfred/neighbor/eslint'
|
|
156
|
-
|
|
157
|
-
export default [
|
|
158
|
-
{
|
|
159
|
-
plugins: { ...neighbor.configs.recommended.plugins },
|
|
160
|
-
rules: { ...neighbor.configs.recommended.rules },
|
|
161
|
-
},
|
|
162
|
-
]
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### Remix 2
|
|
166
|
-
|
|
167
|
-
Remix 2 is React-based. Use the React entry point. The `no-hash-router-in-remix` and `no-use-page-title-in-remix` rules activate automatically when Remix imports are detected.
|
|
168
|
-
|
|
169
|
-
```bash
|
|
170
|
-
npm install --save-dev eslint-plugin-jsx-a11y @a11yfred/neighbor
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
```js
|
|
174
|
-
// eslint.config.js
|
|
175
|
-
import neighbor from '@a11yfred/neighbor/eslint'
|
|
176
|
-
|
|
177
|
-
export default [
|
|
178
|
-
{
|
|
179
|
-
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
180
|
-
plugins: { ...neighbor.configs.recommended.plugins },
|
|
181
|
-
rules: { ...neighbor.configs.recommended.rules },
|
|
182
|
-
},
|
|
183
|
-
]
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Remix 3
|
|
187
|
-
|
|
188
|
-
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.
|
|
189
|
-
|
|
190
|
-
If you are using React with Remix 3:
|
|
191
|
-
|
|
192
|
-
```bash
|
|
193
|
-
npm install --save-dev eslint-plugin-jsx-a11y @a11yfred/neighbor
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
```js
|
|
197
|
-
import neighbor from '@a11yfred/neighbor/eslint'
|
|
198
|
-
|
|
199
|
-
export default [
|
|
200
|
-
{
|
|
201
|
-
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
202
|
-
plugins: { ...neighbor.configs.recommended.plugins },
|
|
203
|
-
rules: { ...neighbor.configs.recommended.rules },
|
|
204
|
-
},
|
|
205
|
-
]
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
If you are not using React with Remix 3, neighbor does not currently have a template-level entry point for your renderer. The Remix-specific rules (`no-hash-router-in-remix`, `no-use-page-title-in-remix`) only apply to React-based Remix projects.
|
|
209
|
-
|
|
210
|
-
### Vue
|
|
211
|
-
|
|
212
|
-
```bash
|
|
213
|
-
npm install --save-dev eslint-plugin-vuejs-accessibility @a11yfred/neighbor
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
```js
|
|
217
|
-
import vueParser from 'vue-eslint-parser'
|
|
218
|
-
import neighbor from '@a11yfred/neighbor/eslint-vue'
|
|
219
|
-
|
|
220
|
-
export default [
|
|
221
|
-
{
|
|
222
|
-
files: ['**/*.vue'],
|
|
223
|
-
languageOptions: { parser: vueParser },
|
|
224
|
-
plugins: { ...neighbor.configs.recommended.plugins },
|
|
225
|
-
rules: { ...neighbor.configs.recommended.rules },
|
|
226
|
-
},
|
|
227
|
-
]
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### Angular
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
npm install --save-dev @angular-eslint/eslint-plugin-template @a11yfred/neighbor
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
```js
|
|
237
|
-
import angularTemplateParser from '@angular-eslint/template-parser'
|
|
238
|
-
import neighbor from '@a11yfred/neighbor/eslint-angular'
|
|
239
|
-
|
|
240
|
-
export default [
|
|
241
|
-
{
|
|
242
|
-
files: ['**/*.html'],
|
|
243
|
-
languageOptions: { parser: angularTemplateParser },
|
|
244
|
-
plugins: { ...neighbor.configs.recommended.plugins },
|
|
245
|
-
rules: { ...neighbor.configs.recommended.rules },
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
// Also lint component TypeScript files for the announce() rule
|
|
249
|
-
files: ['**/*.ts'],
|
|
250
|
-
plugins: { '@a11yfred/neighbor': neighbor },
|
|
251
|
-
rules: {
|
|
252
|
-
'@a11yfred/neighbor/no-announce-in-render': 'error',
|
|
253
|
-
},
|
|
254
|
-
},
|
|
255
|
-
]
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### Stylelint
|
|
259
|
-
|
|
260
|
-
```json
|
|
261
|
-
// .stylelintrc.json
|
|
262
|
-
{
|
|
263
|
-
"plugins": ["@a11yfred/neighbor"],
|
|
264
|
-
"rules": {
|
|
265
|
-
"neighbor/user-preferences": true,
|
|
266
|
-
"neighbor/no-outline-none": true,
|
|
267
|
-
"neighbor/no-forced-colors-none": true
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### Content linting
|
|
273
|
-
|
|
274
|
-
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.
|
|
275
|
-
|
|
276
|
-
```bash
|
|
277
|
-
npm install --save-dev @a11yfred/neighbor
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
```js
|
|
281
|
-
// eslint.config.js
|
|
282
|
-
import neighborContent from '@a11yfred/neighbor/content'
|
|
283
|
-
|
|
284
|
-
export default [
|
|
285
|
-
{
|
|
286
|
-
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
287
|
-
plugins: { ...neighborContent.configs.recommended.plugins },
|
|
288
|
-
rules: { ...neighborContent.configs.recommended.rules },
|
|
289
|
-
},
|
|
290
|
-
]
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
To use alongside the React markup plugin:
|
|
294
|
-
|
|
295
|
-
```js
|
|
296
|
-
// eslint.config.js
|
|
297
|
-
import neighbor from '@a11yfred/neighbor/eslint'
|
|
298
|
-
import neighborContent from '@a11yfred/neighbor/content'
|
|
299
|
-
|
|
300
|
-
export default [
|
|
301
|
-
{
|
|
302
|
-
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
303
|
-
plugins: {
|
|
304
|
-
...neighbor.configs.recommended.plugins,
|
|
305
|
-
...neighborContent.configs.recommended.plugins,
|
|
306
|
-
},
|
|
307
|
-
rules: {
|
|
308
|
-
...neighbor.configs.recommended.rules,
|
|
309
|
-
...neighborContent.configs.recommended.rules,
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
|
-
]
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
## Peer dependencies
|
|
316
|
-
|
|
317
|
-
| Peer | Required for |
|
|
318
|
-
| --- | --- |
|
|
319
|
-
| `eslint >= 8` | Any ESLint entry point |
|
|
320
|
-
| `eslint-plugin-jsx-a11y >= 6` | React config - neighbor extends it, not replaces it |
|
|
321
|
-
| `eslint-plugin-vuejs-accessibility >= 2` | Vue config |
|
|
322
|
-
| `@angular-eslint/eslint-plugin-template >= 17` | Angular config |
|
|
323
|
-
| `stylelint >= 14` | Stylelint config |
|
|
324
|
-
|
|
325
|
-
All peers are optional. Install only what your project uses.
|
|
326
|
-
|
|
327
|
-
## What neighbor adds
|
|
328
|
-
|
|
329
|
-
### ESLint - React / JSX
|
|
330
|
-
|
|
331
|
-
Base: `eslint-plugin-jsx-a11y`
|
|
332
|
-
|
|
333
|
-
| What it checks | Rule | Severity | WCAG SC |
|
|
334
|
-
| --- | --- | --- | --- |
|
|
335
|
-
| `aria-disabled` keeps element reachable | `prefer-aria-disabled` | off | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
336
|
-
| `aria-disabled` must block click handler | `no-unblocked-aria-disabled` | error | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
337
|
-
| `aria-label` on a generic element with no role | `no-aria-label-on-generic` | error | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
|
|
338
|
-
| `role="alert"` overuse | `warn-role-alert` | off | [4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
|
|
339
|
-
| `aria-live="assertive"` outside `role="alert"` | `no-assertive-live-overuse` | error | [4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
|
|
340
|
-
| `role="dialog"` requires accessible name | `no-roles-without-name` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
341
|
-
| `role="group"` with form controls requires name | `no-group-without-name` | error | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
|
|
342
|
-
| `role="tooltip"` requires `id` on the tooltip | `no-tooltip-role-misuse` | warn | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
343
|
-
| `role="application"` disables AT browse mode | `no-application-role` | off | - |
|
|
344
|
-
| `role="grid"` almost always wrong | `no-grid-role` | off | - |
|
|
345
|
-
| `role="menu"` on nav triggers wrong AT mode | `no-menu-role-on-nav` | warn | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
346
|
-
| `role="presentation"` on a focusable element | `no-presentation-on-focusable` | error | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
347
|
-
| `role="log"` must not contain interactive children | `no-log-with-interactive-children` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
348
|
-
| `role="img"` requires accessible name | `no-image-role-without-name` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
349
|
-
| `role="tab"` requires `aria-selected` | `no-tabs-without-structure` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
350
|
-
| `role="tab"` should declare `aria-controls` | `no-tab-without-controls` | off | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
351
|
-
| `role="combobox"` requires `aria-expanded` | `no-combobox-without-expanded` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
352
|
-
| `role="slider"` requires value range attributes | `no-slider-without-range` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
353
|
-
| `role="spinbutton"` requires value range attributes | `no-spinbutton-without-range` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
354
|
-
| `role="listbox"` requires `role="option"` children | `no-listbox-without-option` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
355
|
-
| `role="tree"` requires `role="treeitem"` children | `no-tree-without-treeitem` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
356
|
-
| `role="feed"` requires `role="article"` children | `no-feed-without-article` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
357
|
-
| `aria-hidden="true"` + `role="none"` is redundant | `no-redundant-aria-hidden-with-presentation` | error | - |
|
|
358
|
-
| `aria-roledescription` does not translate | `no-aria-roledescription` | off | - |
|
|
359
|
-
| `aria-readonly` has poor AT support | `no-aria-readonly` | off | - |
|
|
360
|
-
| `aria-owns` on a void element | `no-aria-owns-on-void` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
361
|
-
| `aria-activedescendant` requires a non-empty static ID | `no-aria-activedescendant-without-id` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
362
|
-
| `aria-required` only valid on form-control roles | `no-aria-required-on-non-form` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
363
|
-
| `<a>` with only aria-hidden children | `no-aria-hidden-in-link` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
364
|
-
| `<button>` with only aria-hidden children | `no-empty-button` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
365
|
-
| `<input>` placeholder used as sole label | `no-placeholder-only` | error | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
|
|
366
|
-
| `<input>` with invalid type value | `no-input-type-invalid` | error | [1.3.5](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose) |
|
|
367
|
-
| `<button>` in a form missing explicit type | `no-button-type-missing` | warn | [HTML spec](https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element) |
|
|
368
|
-
| `<summary>` outside `<details>` | `no-summary-without-details` | error | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
369
|
-
| `<a href="#">` used as a button | `no-href-hash` | off | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
370
|
-
| `target="_blank"` without new-tab disclosure | `no-target-blank-without-label` | off | [3.2.2](https://www.w3.org/WAI/WCAG21/Understanding/on-input) |
|
|
371
|
-
| Duplicate `id` breaks ARIA relationships | `no-duplicate-id` | error | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
|
|
372
|
-
| Positive `tabIndex` breaks tab order | `no-positive-tabindex` | error | [2.4.3](https://www.w3.org/WAI/WCAG21/Understanding/focus-order) |
|
|
373
|
-
| Heading inside an interactive element | `no-heading-inside-interactive` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
374
|
-
| `title` attribute as the only accessible name | `no-title-as-label` | error | [2.4.6](https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels) |
|
|
375
|
-
| `<video>` or `<audio autoplay>` without controls | `no-autoplay-without-controls` | error | [1.4.2](https://www.w3.org/WAI/WCAG21/Understanding/audio-control) |
|
|
376
|
-
| Mouse-only events without keyboard equivalents | `no-mouse-only-events` | error | [2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
377
|
-
| `aria-labelledby`/`describedby` references missing `id` | `no-labelledby-missing-target` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
378
|
-
| `dangerouslySetInnerHTML` outside a live region | `no-dynamic-content-without-live` | error | [4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
|
|
379
|
-
| Multiple `<label>` elements for the same control | `form-field-multiple-labels` | error | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
|
|
380
|
-
| `<th>` or header role with no accessible text | `no-empty-table-header` | error | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
|
|
381
|
-
| `announce()` called in component render body | `no-announce-in-render` | error | [4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
|
|
382
|
-
|
|
383
|
-
### ESLint - Remix 2
|
|
384
|
-
|
|
385
|
-
Same as React / JSX. Additional rules activate when Remix imports are detected in the file being linted:
|
|
386
|
-
|
|
387
|
-
| What it checks | Rule | Severity |
|
|
388
|
-
| --- | --- | --- |
|
|
389
|
-
| `@ulam` hash router alongside `react-router` | `no-hash-router-in-remix` | warn |
|
|
390
|
-
| `usePageTitle()` alongside `react-router` | `no-use-page-title-in-remix` | warn |
|
|
391
|
-
|
|
392
|
-
### ESLint - Vue SFCs
|
|
393
|
-
|
|
394
|
-
Base: `eslint-plugin-vuejs-accessibility`
|
|
395
|
-
|
|
396
|
-
Neighbor adds everything in the React table above, adapted for Vue's AST (`v-html` instead of `dangerouslySetInnerHTML`), plus:
|
|
397
|
-
|
|
398
|
-
| What it checks | Rule | Severity | WCAG SC |
|
|
399
|
-
| --- | --- | --- | --- |
|
|
400
|
-
| Ambiguous link text ("click here", "read more") | `no-anchor-ambiguous-text` | error | [2.4.4](https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context) |
|
|
401
|
-
| `<a>` with no content and no accessible name | `no-anchor-no-content` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
402
|
-
| Invalid ARIA attribute values | `no-invalid-aria-prop-value` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
403
|
-
| Invalid `autocomplete` token | `no-autocomplete-invalid` | error | [1.3.5](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose) |
|
|
404
|
-
| Heading with no content | `no-heading-no-content` | error | [2.4.6](https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels) |
|
|
405
|
-
| `<iframe>` without `title` | `no-iframe-no-title` | error | [4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
406
|
-
| Alt text contains "image", "photo" | `no-img-redundant-alt` | warn | [1.1.1](https://www.w3.org/WAI/WCAG21/Understanding/non-text-content) |
|
|
407
|
-
| `accessKey` attribute | `no-access-key` | warn | [2.1.4](https://www.w3.org/WAI/WCAG21/Understanding/character-key-shortcuts) |
|
|
408
|
-
| `scope` on `<td>` (only valid on `<th>`) | `no-scope-on-td` | error | [1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
|
|
409
|
-
| `announce()` called outside `onMounted`/`watch`/handler | `no-announce-in-render` | error | [4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
|
|
410
|
-
|
|
411
|
-
### ESLint - Angular templates
|
|
412
|
-
|
|
413
|
-
Base: `@angular-eslint/eslint-plugin-template`
|
|
414
|
-
|
|
415
|
-
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.
|
|
416
|
-
|
|
417
|
-
**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).
|
|
418
|
-
|
|
419
|
-
### Stylelint - CSS
|
|
420
|
-
|
|
421
|
-
| Rule | Severity | What it checks |
|
|
422
|
-
| --- | --- | --- |
|
|
423
|
-
| `neighbor/user-preferences` | warn | 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) |
|
|
424
|
-
| `neighbor/no-outline-none` | error | Disallows bare `outline: none` or `outline: 0` outside `:focus` selectors - [SC 2.4.7](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible) |
|
|
425
|
-
| `neighbor/no-forced-colors-none` | error | 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) |
|
|
426
|
-
|
|
427
|
-
### Content linter
|
|
428
|
-
|
|
429
|
-
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.
|
|
430
|
-
|
|
431
|
-
| Rule | What it flags | Severity | WCAG SC |
|
|
432
|
-
| --- | --- | --- | --- |
|
|
433
|
-
| `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) |
|
|
434
|
-
| `no-disability-metaphor` | Figurative use of disability language ("blind spot", "tone deaf", "paralyzed by") | warn | - |
|
|
435
|
-
| `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) |
|
|
436
|
-
| `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) |
|
|
437
|
-
| `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) |
|
|
438
|
-
| `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) |
|
|
439
|
-
| `no-all-caps-prose` | ALL CAPS words in prose that screen readers may spell out letter-by-letter | warn | - |
|
|
440
|
-
| `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) |
|
|
441
|
-
| `no-ampersand-in-prose` | `&` used in place of "and" in prose - announced inconsistently by screen readers | warn | - |
|
|
442
|
-
|
|
443
|
-
See [RULES-CONTENT.md](RULES-CONTENT.md) for the full rule reference including sources, methodology, and the language-evolution note.
|
|
444
|
-
|
|
445
|
-
## Rule severity
|
|
446
|
-
|
|
447
|
-
| Severity | Meaning |
|
|
448
|
-
| --- | --- |
|
|
449
|
-
| `error` | Definite AT breakage or HTML spec violation |
|
|
450
|
-
| `warn` | Strong guidance, occasional legitimate overrides exist |
|
|
451
|
-
| `off` | Available but disabled - too noisy for most codebases, enable if it fits your project |
|
|
452
|
-
|
|
453
|
-
All rules can be overridden in your config.
|
|
454
|
-
|
|
455
|
-
## Roadmap
|
|
456
|
-
|
|
457
|
-
Planned improvements and extensions to neighbor:
|
|
458
|
-
|
|
459
|
-
- [ ] **Browser extension: live page linting** — A Chrome/Firefox extension that applies neighbor's accessibility rules to live web content in real-time, highlighting violations inline. Useful for auditors and testers who want to spot issues while testing third-party sites without a build step. Would reuse existing rule logic and integrate with a debug panel UI similar to [@a11yfred/rogers](https://github.com/a11yfred/rogers).
|
|
460
|
-
|
|
461
|
-
## See also
|
|
462
|
-
|
|
463
|
-
- [RULES.md](RULES.md) - rule index across all domains
|
|
464
|
-
- [RULES-MARKUP.md](RULES-MARKUP.md) - full ESLint rule reference (markup)
|
|
465
|
-
- [RULES-CSS.md](RULES-CSS.md) - full Stylelint rule reference (CSS)
|
|
466
|
-
- [RULES-CONTENT.md](RULES-CONTENT.md) - full content rule reference with sources
|
|
467
|
-
- [neighbor-vale](https://github.com/a11yfred/neighbor-vale) - companion Vale package for prose linting in Markdown, MDX, and HTML
|
|
468
|
-
|
|
469
|
-
## License
|
|
470
|
-
|
|
471
|
-
MIT
|
|
472
|
-
|
|
473
|
-
---
|
|
474
|
-
|
|
475
|
-
*Built with help from Claude.*
|
|
5
|
+
> **Note:** This package is part of the a11yfred monorepo.
|
|
6
|
+
>
|
|
7
|
+
> Please see the [main repository README](https://github.com/a11yfred/neighbor) for full documentation, installation instructions, and a complete list of rules.
|