@a11yfred/neighbor 0.3.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 +59 -7
- package/CONTRIBUTING.md +10 -10
- package/README.md +101 -31
- 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 -2413
- 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 -256
- package/package.json +18 -5
package/RULES-CONTENT.md
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# @a11yfred/neighbor - Content Rules
|
|
2
|
+
|
|
3
|
+
Rules for accessible and inclusive web and app copy.
|
|
4
|
+
|
|
5
|
+
→ [Markup rules](RULES-MARKUP.md) · [CSS rules](RULES-CSS.md) · [Back to RULES.md](RULES.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## On language
|
|
10
|
+
|
|
11
|
+
Language is inherently sensitive. What is appropriate - or inappropriate - is never fixed. It shifts depending on who is speaking, who is listening, the relationship between them, the cultural context, the time period, and how communities themselves evolve. A term that was clinical yesterday may be reclaimed tomorrow. A word considered polite in one country may carry different weight in another. What one person finds empowering another may find reductive.
|
|
12
|
+
|
|
13
|
+
This is not a problem that a linter can fully solve. It is a problem that requires ongoing human attention.
|
|
14
|
+
|
|
15
|
+
Accessibility practitioner [Nicolas Steenhout](https://incl.ca/disability-language-is-a-nuanced-thing/) argues against prescriptive language rules: disabled people must lead conversations about disability language rather than having terminology imposed by well-meaning non-disabled people - the foundational disability rights principle *[Nothing About Us Without Us](https://en.wikipedia.org/wiki/Nothing_About_Us_Without_Us)*. Well-intentioned euphemisms ("handicapable", "physically challenged") have historically increased stigma rather than reduced it, precisely because they were invented by people outside the community they were meant to serve.
|
|
16
|
+
|
|
17
|
+
Steenhout cites [Léonie Watson](https://tink.uk), blind web standards engineer: *"There is no right or wrong answer because it is a matter of personal choice, and the choice depends on context."*
|
|
18
|
+
|
|
19
|
+
With that framing in mind, these rules exist to flag patterns where expert consensus across multiple independent disability-led sources is clear and consistent - not to arbitrate language for every situation. Where consensus is genuinely contested (identity-first vs person-first language being the clearest example), no rule is applied. Where context matters more than pattern (a slur used in direct quotation, a metaphor in a novel excerpt, internal tooling with a known audience), suppress the rule.
|
|
20
|
+
|
|
21
|
+
All content rules ship as `warn`, not `error`, for exactly this reason. A warning is an invitation to think. An error is a claim of certainty that language does not deserve.
|
|
22
|
+
|
|
23
|
+
If the defaults feel wrong for your community, context, or codebase - use the `allow` option, open an issue, or submit a PR. These lists should be a living document maintained by the people who use them.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Rule methodology
|
|
28
|
+
|
|
29
|
+
A rule is included only when all three conditions hold:
|
|
30
|
+
|
|
31
|
+
1. A WCAG Success Criterion directly applies, **or** the pattern appears in ≥ 3 independent authoritative sources as an explicit problem.
|
|
32
|
+
2. The rule can be expressed as a finite, deterministic pattern - a string match, token count, or AST shape. No NLP, no runtime context required.
|
|
33
|
+
3. Expert consensus is clear and consistent across sources. Where credible authorities disagree, the rule is excluded.
|
|
34
|
+
|
|
35
|
+
Rules that require subjective reading, that depend on the relationship between speaker and audience, or that are under active community debate are not included.
|
|
36
|
+
|
|
37
|
+
Grammarly and the Hemingway Editor informed the sentence-structure patterns: both flag passive voice, sentences over ~25 words, and words with simpler alternatives. Those patterns are consistent with the plain language guides surveyed. They are noted in the "Rules not included" section because, while valid as prose guidance, their false-positive rate in code string literals is too high to ship by default.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Sources
|
|
42
|
+
|
|
43
|
+
These rules were synthesized from the following sources. Where sources conflict, W3C WAI takes precedence.
|
|
44
|
+
|
|
45
|
+
### Global standards
|
|
46
|
+
|
|
47
|
+
| Source | URL |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| W3C WAI Writing Tips | [w3.org/WAI/tips/writing](https://www.w3.org/WAI/tips/writing/) - primary authority |
|
|
50
|
+
| wcag.com/authors | [wcag.com/authors](https://wcag.com/authors/) |
|
|
51
|
+
| WCAG 2.2 | [w3.org/TR/WCAG22](https://www.w3.org/TR/WCAG22/) |
|
|
52
|
+
|
|
53
|
+
### English-speaking governments
|
|
54
|
+
|
|
55
|
+
| Country | Source |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| United States | [plainlanguage.gov](https://www.plainlanguage.gov) / [digital.gov/guides/plain-language](https://digital.gov/guides/plain-language) |
|
|
58
|
+
| United States | [SBA Content Style Guide](https://advocacy.sba.gov/office-of-advocacy-content-style-guide/writing-accessible-content/) |
|
|
59
|
+
| United Kingdom | [GOV.UK - Publishing Accessible Documents](https://www.gov.uk/guidance/publishing-accessible-documents) |
|
|
60
|
+
| United Kingdom | [DWP Accessibility Manual](https://accessibility-manual.dwp.gov.uk/best-practice/writing-content) |
|
|
61
|
+
| United Kingdom | [GOV.UK Communications - accessible communications resources](https://www.communications.gov.uk/guidance/accessible-communications/accessible-communications-learning-and-resources/) |
|
|
62
|
+
| Australia | [Australian Government Style Manual - Accessible and Inclusive Content](https://www.stylemanual.gov.au/accessible-and-inclusive-content) |
|
|
63
|
+
| Canada | [Government of Canada - Guidelines for Creating Accessible Documents](https://accessible.canada.ca/guidelines-creating-accessible-documents) |
|
|
64
|
+
|
|
65
|
+
### Disability language authorities
|
|
66
|
+
|
|
67
|
+
| Source | URL | Notes |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| NCDJ Disability Language Style Guide | [cronkite.asu.edu/ncdj](https://cronkite.asu.edu/ncdj/disability-language-style-guide) | Journalism standard; updated regularly |
|
|
70
|
+
| AP Stylebook - Disability | [amdisrights.org/ap-stylebook-primer-on-disability](https://amdisrights.org/ap-stylebook-primer-on-disability) | Wire journalism standard |
|
|
71
|
+
| ADA National Network | [adata.org/factsheet/ADANN-writing](https://adata.org/factsheet/ADANN-writing) | U.S. legal/advocacy context |
|
|
72
|
+
| APA Style - Disability | [apastyle.apa.org - bias-free language](https://apastyle.apa.org/style-grammar-guidelines/bias-free-language/disability) | Academic publishing standard |
|
|
73
|
+
| SIGACCESS Accessible Writing Guide | [sigaccess.org](https://www.sigaccess.org/welcome-to-sigaccess/resources/accessible-writing-guide/) | Computing research community |
|
|
74
|
+
| Nicolas Steenhout | [incl.ca - Disability Language Is a Nuanced Thing](https://incl.ca/disability-language-is-a-nuanced-thing/) | Practitioner perspective; *Nothing About Us Without Us* principle; identity-first vs person-first as community choice, not external rule; cites Léonie Watson |
|
|
75
|
+
| Léonie Watson | [tink.uk](https://tink.uk) | Blind web standards engineer; cited by Steenhout: *"There is no right or wrong answer because it is a matter of personal choice, and the choice depends on context."* |
|
|
76
|
+
|
|
77
|
+
### Technical and UX writing
|
|
78
|
+
|
|
79
|
+
| Source | URL |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| Google Developer Style Guide | [developers.google.com/style/accessibility](https://developers.google.com/style/accessibility) |
|
|
82
|
+
| UX Content Co. | [uxcontent.com - Accessible UX Writing](https://uxcontent.com/accessible-ux-writing-a-guide-for-inclusive-content-design/) |
|
|
83
|
+
| A11y Collective | [a11y-collective.com - Accessible Writing](https://www.a11y-collective.com/blog/accessible-writing/) |
|
|
84
|
+
| SJSU Writing Center | [sjsu.edu - Accessible Writing Strategies](https://www.sjsu.edu/writingcenter/docs/handouts/Accessible%20Writing%20Strategies.pdf) |
|
|
85
|
+
| Section 508 | [section508.gov - Alternative Text](https://www.section508.gov/create/alternative-text/) |
|
|
86
|
+
| Grammarly | Clarity and passive voice patterns |
|
|
87
|
+
| Hemingway Editor | Sentence length and readability grade |
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Rules
|
|
92
|
+
|
|
93
|
+
All rules ship from `@a11yfred/neighbor/content`. All ship as `warn` by default.
|
|
94
|
+
|
|
95
|
+
**Why all warnings?** Content is subjective in ways markup is not. A rule that fires on a metaphor inside a novel excerpt, or on an idiom in a developer-facing internal tool, is noise. Every content rule has legitimate exceptions - `warn` lets teams decide which matter for their context rather than forcing blanket errors. Upgrade individual rules to `error` in your own config where the stakes are higher.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### Disability language
|
|
100
|
+
|
|
101
|
+
#### `no-ableist-language`
|
|
102
|
+
|
|
103
|
+
Flags slurs, condescending euphemisms, and suffering-framing when writing about disability.
|
|
104
|
+
|
|
105
|
+
**WCAG basis:** SC 3.1.1 (Language of Page). While WCAG does not enumerate specific words, content that demeans or excludes users undermines the perceivable and understandable principles the spec is built on.
|
|
106
|
+
|
|
107
|
+
**Consensus:** Every disability language guide surveyed - NCDJ, AP Stylebook, ADA National Network, APA Style, SIGACCESS - independently prohibits these terms. No credible source defends them.
|
|
108
|
+
|
|
109
|
+
**What it catches:**
|
|
110
|
+
|
|
111
|
+
| Avoid | Instead use | Sources |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| cripple / crippled | person with a mobility disability | NCDJ, AP, ADA NN, APA |
|
|
114
|
+
| retarded / retard | person with an intellectual disability | NCDJ, AP, ADA NN, APA |
|
|
115
|
+
| dumb | mute / nonverbal | NCDJ, ADA NN |
|
|
116
|
+
| lame | weak / unconvincing | NCDJ, A11y Collective |
|
|
117
|
+
| special needs | disability / person with a disability | NCDJ, AP, ADA NN, APA |
|
|
118
|
+
| differently abled | person with a disability | NCDJ, AP, ADA NN, APA |
|
|
119
|
+
| handi-capable / physically challenged | person with a disability | NCDJ, AP, ADA NN |
|
|
120
|
+
| wheelchair-bound / confined to a wheelchair | wheelchair user | NCDJ, AP, ADA NN, APA, SIGACCESS |
|
|
121
|
+
| suffers from | has / lives with | NCDJ, AP, ADA NN, APA |
|
|
122
|
+
| afflicted with / victim of | has / lives with | NCDJ, AP, ADA NN, APA |
|
|
123
|
+
| committed suicide | died by suicide | ADA NN, APA, AP Stylebook 2022 |
|
|
124
|
+
| crazy / psycho | wild / reckless (in non-clinical use) | NCDJ, A11y Collective |
|
|
125
|
+
| hearing-impaired | deaf / hard of hearing | NCDJ, AP, ADA NN, APA |
|
|
126
|
+
| the disabled / the blind / the deaf | people with disabilities / blind people / deaf people | NCDJ, AP, ADA NN, APA, SIGACCESS |
|
|
127
|
+
| normal people / normal hearing | people without disabilities / typical hearing | AP, ADA NN, APA, SIGACCESS |
|
|
128
|
+
| handicapped | person with a disability / accessible (for spaces) | NCDJ, AP, ADA NN |
|
|
129
|
+
|
|
130
|
+
**Identity-first vs person-first language:** "Autistic person" and "person with autism" are both used in disability communities. APA (2022) accepts both and recommends following individual preference. [Nicolas Steenhout](https://incl.ca/disability-language-is-a-nuanced-thing/) notes the current momentum in disability advocacy is toward identity-first language as reclamation, while person-first remains standard in many clinical and government contexts. [Léonie Watson](https://tink.uk), cited by Steenhout: *"There is no right or wrong answer because it is a matter of personal choice, and the choice depends on context."* This rule does not flag either form.
|
|
131
|
+
|
|
132
|
+
**Configuration:**
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
'@a11yfred/neighbor/content/no-ableist-language': ['warn', {
|
|
136
|
+
allow: ['crazy-good'] // strings to suppress
|
|
137
|
+
}]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
#### `no-disability-metaphor`
|
|
143
|
+
|
|
144
|
+
Flags figurative uses of disability language - disability used as a metaphor in non-clinical prose.
|
|
145
|
+
|
|
146
|
+
**WCAG basis:** No direct SC. Grounded in NCDJ, A11y Collective, and APA guidance that these uses normalise disability as a negative even when not intended that way.
|
|
147
|
+
|
|
148
|
+
**What it catches:**
|
|
149
|
+
|
|
150
|
+
| Avoid | Instead use | Sources |
|
|
151
|
+
| --- | --- | --- |
|
|
152
|
+
| blind spot | gap / oversight / unaware of | NCDJ, A11y Collective |
|
|
153
|
+
| turning a blind eye | ignoring / overlooking | NCDJ, A11y Collective |
|
|
154
|
+
| tone deaf | out of touch / insensitive | NCDJ, A11y Collective |
|
|
155
|
+
| falling on deaf ears | being ignored / going unheard | NCDJ, A11y Collective |
|
|
156
|
+
| paralyzed by / paralyzed with | overwhelmed by / unable to act because of | NCDJ, A11y Collective |
|
|
157
|
+
| crippling debt / crippling fear | devastating / crushing | NCDJ, A11y Collective |
|
|
158
|
+
| schizophrenic approach | contradictory / inconsistent | NCDJ, APA |
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### Clarity and plain language
|
|
163
|
+
|
|
164
|
+
#### `no-english-idiom`
|
|
165
|
+
|
|
166
|
+
Flags English idioms and sports metaphors that are opaque to ESL readers and international audiences.
|
|
167
|
+
|
|
168
|
+
**WCAG basis:** SC 3.1.5 (Reading Level). Idioms systematically fail this criterion for non-native English speakers because their meaning cannot be inferred from constituent words. No other accessibility linting tool flags idioms - this is the most novel rule in this set.
|
|
169
|
+
|
|
170
|
+
**Sources:** Canadian Government accessible documents guide, SJSU accessible writing strategies, UX Content Co., A11y Collective.
|
|
171
|
+
|
|
172
|
+
**What it catches:**
|
|
173
|
+
|
|
174
|
+
| Avoid | Instead use |
|
|
175
|
+
| --- | --- |
|
|
176
|
+
| boil the ocean | attempt everything at once |
|
|
177
|
+
| move the needle | make progress / have an impact |
|
|
178
|
+
| blue-sky thinking | open-ended brainstorming |
|
|
179
|
+
| drink the Kool-Aid | follow without question |
|
|
180
|
+
| low-hanging fruit | easiest tasks / quick wins |
|
|
181
|
+
| circle back | follow up / return to |
|
|
182
|
+
| take it offline | discuss separately |
|
|
183
|
+
| deep dive | thorough review |
|
|
184
|
+
| level-set | align / agree on expectations |
|
|
185
|
+
| back to square one | starting over |
|
|
186
|
+
| in the pipeline | planned / in progress |
|
|
187
|
+
| on the same page | in agreement |
|
|
188
|
+
| catch-22 | impossible situation |
|
|
189
|
+
| hit the ground running | start immediately |
|
|
190
|
+
| on the fence | undecided |
|
|
191
|
+
| bite the bullet | proceed despite difficulty |
|
|
192
|
+
| under the weather | unwell / sick |
|
|
193
|
+
| ballpark | rough estimate |
|
|
194
|
+
| slam dunk | certain success |
|
|
195
|
+
| drop the ball | make a mistake |
|
|
196
|
+
| game-changer | major shift |
|
|
197
|
+
| level the playing field | create equal conditions |
|
|
198
|
+
| move the goalposts | change the requirements |
|
|
199
|
+
| touch base | check in / follow up |
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
#### `no-directional-language`
|
|
204
|
+
|
|
205
|
+
Flags content that gives instructions using screen position ("above", "in the right sidebar").
|
|
206
|
+
|
|
207
|
+
**WCAG basis:** SC 1.3.3 (Sensory Characteristics) - instructions shall not rely solely on location or sensory characteristics. Position references break for screen reader users, keyboard users, and anyone who zooms or reflows the page.
|
|
208
|
+
|
|
209
|
+
**Sources:** SBA Content Style Guide, Google Developer Style Guide, WCAG SC 1.3.3.
|
|
210
|
+
|
|
211
|
+
**What it catches:** "see above", "in the right sidebar", "refer to the table below", "as shown above", "in the left column".
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
#### `no-unexplained-abbreviation`
|
|
216
|
+
|
|
217
|
+
Flags abbreviations and acronyms used without a prior expansion in the same file.
|
|
218
|
+
|
|
219
|
+
**WCAG basis:** SC 3.1.4 (Abbreviations) - a mechanism shall be available for identifying the expanded form of abbreviations.
|
|
220
|
+
|
|
221
|
+
**Sources:** Google Developer Style Guide, GOV.UK, wcag.com/authors, SBA, US Plain Language, Canadian Government.
|
|
222
|
+
|
|
223
|
+
**Configuration:** Add project-specific known abbreviations to suppress:
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
'@a11yfred/neighbor/content/no-unexplained-abbreviation': ['warn', {
|
|
227
|
+
known: ['CMS', 'HIPAA', 'FHIR']
|
|
228
|
+
}]
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
#### `no-all-caps-prose`
|
|
234
|
+
|
|
235
|
+
Flags ALL CAPS words in prose content.
|
|
236
|
+
|
|
237
|
+
**Why it matters:** Some screen readers using high verbosity settings read ALL CAPS letter-by-letter ("H-E-L-P" instead of "help"). Also reduces readability for users with dyslexia. IMPORTANT, WARNING, NOTE, and common acronyms are excluded by default.
|
|
238
|
+
|
|
239
|
+
**Sources:** Google Developer Style Guide, GOV.UK publishing guide, Canadian Government guide.
|
|
240
|
+
|
|
241
|
+
**Configuration:**
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
'@a11yfred/neighbor/content/no-all-caps-prose': ['warn', {
|
|
245
|
+
known: ['GDPR', 'CCPA'] // additional known acronyms to allow
|
|
246
|
+
}]
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
#### `no-ampersand-in-prose`
|
|
252
|
+
|
|
253
|
+
Flags `&` used in place of "and" in prose.
|
|
254
|
+
|
|
255
|
+
**Why it matters:** Screen readers may announce `&` as "ampersand" or skip it entirely - behaviour is inconsistent across AT vendors and verbosity settings.
|
|
256
|
+
|
|
257
|
+
**Sources:** Google Developer Style Guide, US Plain Language guide.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### UX copy and error messages
|
|
262
|
+
|
|
263
|
+
#### `no-vague-cta`
|
|
264
|
+
|
|
265
|
+
Flags vague call-to-action and link text.
|
|
266
|
+
|
|
267
|
+
**WCAG basis:** SC 2.4.4 (Link Purpose, In Context) - link purpose shall be determinable from the link text alone. Patterns like "click here" or "read more" are the most-cited failure in the annual WebAIM Million report.
|
|
268
|
+
|
|
269
|
+
**Sources:** W3C WAI, wcag.com/authors, Google Developer Style Guide, SBA, UX Content Co., A11y Collective, GOV.UK, WebAIM Million.
|
|
270
|
+
|
|
271
|
+
**What it catches:** "click here", "here", "read more", "learn more", "more", "this", "link", "tap here", "go", "details", "info", "information".
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
#### `no-vague-error-message`
|
|
276
|
+
|
|
277
|
+
Flags error messages that do not explain what went wrong.
|
|
278
|
+
|
|
279
|
+
**WCAG basis:** SC 3.3.1 (Error Identification) - if an input error is detected, the item in error shall be described. SC 3.3.3 (Error Suggestion) - suggestions for correction shall be provided. "An error occurred" satisfies neither.
|
|
280
|
+
|
|
281
|
+
**Sources:** UX Content Co., Google Developer Style Guide.
|
|
282
|
+
|
|
283
|
+
**What it catches:** "An error occurred", "Something went wrong", "Error", "Unknown error", "Unexpected error", "Oops", "Request failed", "Operation failed", "Please try again".
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Rules not included
|
|
288
|
+
|
|
289
|
+
| Pattern | Reason not included |
|
|
290
|
+
| --- | --- |
|
|
291
|
+
| Passive voice | Hemingway and Grammarly both flag this, and the plain language guides recommend active voice. However, passive voice has many legitimate uses in technical and legal writing. The false-positive rate is high enough that it would generate more noise than signal for most codebases. Recommended alternative: use Grammarly or Hemingway for prose review outside the linter. |
|
|
292
|
+
| Sentence length | A 25-word threshold is the most commonly cited guideline (Google Dev Style, GOV.UK). However, compound technical sentences often need to exceed this. A sentence-length rule would require calibration per content type and is better suited to a prose editor than a code linter. |
|
|
293
|
+
| Reading grade level | Cannot be computed accurately from string literals in a JS AST without analysing full document context. Better measured by Hemingway on full page text. |
|
|
294
|
+
| Adverbs and qualifiers ("very", "really", "quite") | Flagged by Grammarly and Hemingway as weak writing. Not an accessibility-specific issue and false-positive rate in code string literals is extremely high. |
|
|
295
|
+
| Cultural references | Too broad to enumerate reliably. No finite term list is possible. |
|
|
296
|
+
| Placeholder used as label | Overlaps with `jsx-a11y/label-has-associated-control`. Check that rule first before enabling here. |
|
package/RULES-CSS.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @a11yfred/neighbor - CSS Rules
|
|
2
|
+
|
|
3
|
+
Stylelint rules for CSS accessibility.
|
|
4
|
+
|
|
5
|
+
→ [Markup rules](RULES-MARKUP.md) · [Content rules](RULES-CONTENT.md) · [Back to RULES.md](RULES.md)
|
|
6
|
+
|
|
7
|
+
## Sources and credits
|
|
8
|
+
|
|
9
|
+
| Source | Reference |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| WCAG 2.1 SC 1.4.3 | [Contrast (Minimum)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum) |
|
|
12
|
+
| WCAG 2.1 SC 1.4.4 | [Resize Text](https://www.w3.org/WAI/WCAG21/Understanding/resize-text) |
|
|
13
|
+
| WCAG 2.1 SC 1.4.11 | [Non-text Contrast](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast) |
|
|
14
|
+
| WCAG 2.1 SC 2.3.3 | [Animation from Interactions](https://www.w3.org/WAI/WCAG21/Understanding/animation-from-interactions) |
|
|
15
|
+
| WCAG 2.1 SC 2.4.7 | [Focus Visible](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible) |
|
|
16
|
+
| Eric Eggert | [yatil.net](https://yatil.net) - forced colors and focus patterns |
|
|
17
|
+
| MDN Web Docs | [forced-color-adjust](https://developer.mozilla.org/en-US/docs/Web/CSS/forced-color-adjust) |
|
|
18
|
+
| double-great/stylelint-a11y | [github.com/double-great/stylelint-a11y](https://github.com/double-great/stylelint-a11y) |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Rules
|
|
23
|
+
|
|
24
|
+
All CSS rules use the `neighbor/` namespace and ship from `@a11yfred/neighbor` (the default entry point) and `@a11yfred/neighbor/stylelint`.
|
|
25
|
+
|
|
26
|
+
### Warnings - on by default
|
|
27
|
+
|
|
28
|
+
| Rule | What it flags | WCAG SC |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `neighbor/user-preferences` | `opacity`, `animation`, `transition`, or alpha-channel colors used in `src/components/ui/` without a `@media (prefers-reduced-motion)`, `@media (prefers-reduced-transparency)`, or `@media (forced-colors)` counterpart | [SC 1.4.3](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum) / [SC 2.3.3](https://www.w3.org/WAI/WCAG21/Understanding/animation-from-interactions) |
|
|
31
|
+
| `neighbor/no-outline-none` | `outline: none` or `outline: 0` in a base rule (outside a `:focus`, `:focus-visible`, or `:focus-within` selector) - removes the keyboard focus indicator for all users | [SC 2.4.7](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible) |
|
|
32
|
+
| `neighbor/no-forced-colors-none` | `forced-color-adjust: none` inside `@media (forced-colors)` - actively opts out of Windows High Contrast Mode, removing the system-enforced visibility that users with low vision depend on | [SC 1.4.3](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum) / [SC 1.4.11](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast) |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Notes
|
|
37
|
+
|
|
38
|
+
### `neighbor/no-outline-none`
|
|
39
|
+
|
|
40
|
+
The rule allows `outline: none` inside `:focus`, `:focus-visible`, and `:focus-within` selectors - those are intentional restylings, not removals. The pattern for programmatic-focus-only targets (skip-link destinations, dialog headings) is:
|
|
41
|
+
|
|
42
|
+
```css
|
|
43
|
+
:focus:not(:focus-visible) { outline: none }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This suppresses the visible ring for JS `.focus()` calls while preserving it for keyboard-initiated focus. That pattern is not flagged.
|
|
47
|
+
|
|
48
|
+
### `neighbor/no-forced-colors-none`
|
|
49
|
+
|
|
50
|
+
`forced-color-adjust: none` has a small number of valid uses (color pickers, custom border tricks) when placed *outside* a `@media (forced-colors)` block - those are not flagged. The rule only fires inside the media query, where the intent is explicitly to cancel High Contrast Mode for an element that needs it most.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Rules considered and rejected
|
|
55
|
+
|
|
56
|
+
| Rule | Reason rejected |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `prefer-focus-visible` | `:focus` alone satisfies [SC 2.4.7](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible); flagging it without `:focus-visible` fires constantly on legitimate code |
|
|
59
|
+
| `no-fixed-font-size-px` | Browser zoom satisfies [SC 1.4.4](https://www.w3.org/WAI/WCAG21/Understanding/resize-text) regardless of unit; WCAG is ambiguous on this; very high false-positive rate |
|
|
60
|
+
| `font-size-is-readable` / `no-spread-text` | No universal threshold - highly context-dependent; high false-positive rate on real design systems |
|
|
61
|
+
| `no-forced-colors-none` (global, not scoped to media query) | Legitimate narrow uses exist; rule is scoped to `@media (forced-colors)` blocks only |
|
package/RULES-MARKUP.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# @a11yfred/neighbor - Markup Rules
|
|
2
|
+
|
|
3
|
+
ESLint rules for React / JSX, Vue SFCs, and Angular templates.
|
|
4
|
+
|
|
5
|
+
→ [CSS rules](RULES-CSS.md) · [Content rules](RULES-CONTENT.md) · [Back to RULES.md](RULES.md)
|
|
6
|
+
|
|
7
|
+
## Sources and credits
|
|
8
|
+
|
|
9
|
+
| Source | Reference |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Adrian Roselli | [adrianroselli.com](https://adrianroselli.com) |
|
|
12
|
+
| Heydon Pickering | [heydonworks.com](https://heydonworks.com), [inclusive-components.design](https://inclusive-components.design) |
|
|
13
|
+
| Scott O'Hara | [scottohara.me](https://scottohara.me) |
|
|
14
|
+
| Patrick Lauke | [splintered.co.uk](https://splintered.co.uk), [patrickhlauke.github.io/aria](https://patrickhlauke.github.io/aria) |
|
|
15
|
+
| Karl Groves | [karlgroves.com](https://karlgroves.com) |
|
|
16
|
+
| Marcy Sutton | [marcysutton.com](https://marcysutton.com) |
|
|
17
|
+
| Eric Eggert | [yatil.net](https://yatil.net) |
|
|
18
|
+
| WAI-ARIA APG | [w3.org/WAI/ARIA/apg](https://www.w3.org/WAI/ARIA/apg/) |
|
|
19
|
+
| ARIA 1.2 spec | [w3.org/TR/wai-aria-1.2](https://www.w3.org/TR/wai-aria-1.2/) |
|
|
20
|
+
| WebAIM Million | [webaim.org/projects/million](https://webaim.org/projects/million/) |
|
|
21
|
+
| Deque / axe-core | deque.com - rule concepts reimplemented independently under MPL-2.0 |
|
|
22
|
+
| WCAG 2.1 | [w3.org/TR/WCAG21](https://www.w3.org/TR/WCAG21/) |
|
|
23
|
+
| WCAG 2.2 | [w3.org/TR/WCAG22](https://www.w3.org/TR/WCAG22/) |
|
|
24
|
+
| HTML Living Standard | [html.spec.whatwg.org](https://html.spec.whatwg.org/) |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Core rules - all frameworks
|
|
29
|
+
|
|
30
|
+
All rules run on React, Vue, and Angular unless noted.
|
|
31
|
+
|
|
32
|
+
### Errors - definite breakage or phantom controls
|
|
33
|
+
|
|
34
|
+
| Rule | What it flags | Source |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `no-aria-label-on-generic` | `aria-label`/`aria-labelledby` on `<div>`, `<span>`, `<p>` with no `role` - AT ignores it | Roselli / O'Hara |
|
|
37
|
+
| `no-assertive-live-overuse` | `aria-live="assertive"` without `role="alert"` - interrupts user unexpectedly | [APG](https://www.w3.org/WAI/ARIA/apg/) / Sutton / Eggert |
|
|
38
|
+
| `no-unblocked-aria-disabled` | `aria-disabled="true"` on an interactive element that still has an `onClick` - clicks still fire | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
|
|
39
|
+
| `no-roles-without-name` | `role="region/dialog/alertdialog/application/marquee/searchbox"` without `aria-label`/`aria-labelledby` | [APG](https://www.w3.org/WAI/ARIA/apg/) / [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
|
|
40
|
+
| `no-group-without-name` | `role="group"` containing form controls without an accessible name | [APG](https://www.w3.org/WAI/ARIA/apg/) / Groves - [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
|
|
41
|
+
| `no-presentation-on-focusable` | `role="presentation"/"none"` on a focusable element - phantom control | Roselli / Lauke / O'Hara - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
42
|
+
| `no-log-with-interactive-children` | Interactive elements nested inside `role="log"` | [APG: Log Role](https://www.w3.org/WAI/ARIA/apg/patterns/) |
|
|
43
|
+
| `no-aria-hidden-in-link` | `<a>` whose only content is `aria-hidden` elements - phantom link with no name | Roselli - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
44
|
+
| `no-redundant-aria-hidden-with-presentation` | `aria-hidden="true"` combined with `role="none"/"presentation"` - redundant | O'Hara |
|
|
45
|
+
| `no-aria-owns-on-void` | `aria-owns` on void elements (`<img>`, `<input>`, `<br>`, etc.) - meaningless | O'Hara / [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
|
|
46
|
+
| `no-title-as-label` | `title` attribute as the sole accessible name on an `<input>` - not keyboard accessible | Groves / O'Hara - [SC 2.4.6](https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels) |
|
|
47
|
+
| `no-tabs-without-structure` | `role="tab"` without `aria-selected`; `role="tabpanel"` without `aria-labelledby`; `role="tablist"` without an accessible name | [APG: Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
48
|
+
| `no-positive-tabindex` | `tabIndex` value greater than 0 - breaks natural DOM tab order | WebAIM / Lauke - [SC 2.4.3](https://www.w3.org/WAI/WCAG21/Understanding/focus-order) |
|
|
49
|
+
| `no-autoplay-without-controls` | `<video>`/`<audio autoPlay>` without `controls` | [SC 1.4.2](https://www.w3.org/WAI/WCAG21/Understanding/audio-control) |
|
|
50
|
+
| `no-heading-inside-interactive` | Heading elements (`<h1>`–`<h6>`) nested inside `<button>`, `<a>`, or interactive roles | Roselli / Pickering - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
51
|
+
| `no-placeholder-only` | `<input placeholder>` with no `aria-label`, `aria-labelledby`, or paired `<label>` - WebAIM Million #3 failure | [WebAIM Million](https://webaim.org/projects/million/) - [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
|
|
52
|
+
| `no-empty-button` | `<button>` with only `aria-hidden` children and no accessible name | [WebAIM Million](https://webaim.org/projects/million/) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
53
|
+
| `no-image-role-without-name` | `role="img"` without `aria-label`/`aria-labelledby` | [APG](https://www.w3.org/WAI/ARIA/apg/) / O'Hara - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
54
|
+
| `no-spinbutton-without-range` | `role="spinbutton"` missing `aria-valuenow`, `aria-valuemin`, or `aria-valuemax` | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Spinbutton](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/) |
|
|
55
|
+
| `no-slider-without-range` | `role="slider"` missing `aria-valuenow`, `aria-valuemin`, or `aria-valuemax` | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) |
|
|
56
|
+
| `no-combobox-without-expanded` | `role="combobox"` without `aria-expanded` | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) |
|
|
57
|
+
| `no-mouse-only-events` | `onMouseEnter`/`onMouseLeave`/`onMouseOver`/`onMouseOut` without `onFocus`/`onBlur` equivalents | [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
58
|
+
| `no-listbox-without-option` | `role="listbox"` with no `role="option"` children | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Listbox](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) |
|
|
59
|
+
| `no-tree-without-treeitem` | `role="tree"` with no `role="treeitem"` children | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Tree View](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) |
|
|
60
|
+
| `no-feed-without-article` | `role="feed"` with no `role="article"` children | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) / [APG: Feed](https://www.w3.org/WAI/ARIA/apg/patterns/feed/) |
|
|
61
|
+
| `no-aria-activedescendant-without-id` | `aria-activedescendant` with an empty or missing static ID | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
62
|
+
| `no-duplicate-id` | Duplicate `id` values on elements referenced by `aria-labelledby`/`describedby`/`controls`/`owns`/`activedescendant` | [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) / [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
63
|
+
| `no-summary-without-details` | `<summary>` outside `<details>` - phantom interactive element | [HTML spec](https://html.spec.whatwg.org/multipage/interactive-elements.html#the-summary-element) - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
64
|
+
| `no-aria-required-on-non-form` | `aria-required` on an element whose role doesn't support it - AT ignores it | [ARIA 1.2 §6.6.9](https://www.w3.org/TR/wai-aria-1.2/#aria-required) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
65
|
+
| `no-input-type-invalid` | `<input type="X">` with an invalid type - silently falls back to `type="text"` | [HTML spec §4.10.18](https://html.spec.whatwg.org/multipage/input.html#the-input-element) - [SC 1.3.5](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose) |
|
|
66
|
+
| `no-labelledby-missing-target` | `aria-labelledby`/`describedby`/`controls`/`owns`/`activedescendant` referencing an `id` that doesn't exist in the file | [ARIA 1.2 §6.2.4](https://www.w3.org/TR/wai-aria-1.2/#mapping_additional_nd_name) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
67
|
+
| `no-dynamic-content-without-live` | `dangerouslySetInnerHTML` / `v-html` / `[innerHTML]` on an element outside a live region | [SC 4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
|
|
68
|
+
| `form-field-multiple-labels` | Multiple `<label for="…">` elements targeting the same input | [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
|
|
69
|
+
| `no-empty-table-header` | `<th>` or `role="columnheader"/"rowheader"` with no accessible text or `aria-label` | [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
|
|
70
|
+
|
|
71
|
+
### Warnings - on by default
|
|
72
|
+
|
|
73
|
+
| Rule | What it flags | Source |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `no-tooltip-role-misuse` | `role="tooltip"` without an `id`; or `role="tooltip"` on an interactive element | [APG: Tooltip Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/) - [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
76
|
+
| `no-menu-role-on-nav` | Menu/menubar/menuitem roles - triggers AT application-mode keyboard handling; especially wrong on `<nav>` | Roselli / Lauke / Groves - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
77
|
+
| `no-button-type-missing` | `<button>` inside a `<form>` without an explicit `type` - defaults to `type="submit"` | [HTML spec §4.10.18](https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element) |
|
|
78
|
+
|
|
79
|
+
### Off by default - opt in
|
|
80
|
+
|
|
81
|
+
These rules flag real problems but generate enough noise in typical codebases that they ship off. Enable individually.
|
|
82
|
+
|
|
83
|
+
| Rule | What it flags | Source |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `no-application-role` | `role="application"` - disables AT browse mode | Roselli / Sutton / Lauke / [APG](https://www.w3.org/WAI/ARIA/apg/) |
|
|
86
|
+
| `no-grid-role` | `role="grid"` - almost always wrong outside spreadsheet-like widgets | Roselli: ARIA Grid As an Anti-Pattern |
|
|
87
|
+
| `no-aria-roledescription` | `aria-roledescription` - overrides AT role label, does not auto-translate | Roselli: Avoid aria-roledescription |
|
|
88
|
+
| `no-aria-readonly` | `aria-readonly` - limited and inconsistent AT support | Roselli |
|
|
89
|
+
| `no-tab-without-controls` | `role="tab"` without `aria-controls` | [APG: Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) |
|
|
90
|
+
| `no-href-hash` | `<a href="#">` used as a button | Sutton: Links vs Buttons - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
91
|
+
| `warn-role-alert` | `role="alert"` - prefer `role="status"` for non-urgent updates | [APG](https://www.w3.org/WAI/ARIA/apg/) / Roselli / Sutton - [SC 4.1.3](https://www.w3.org/WAI/WCAG21/Understanding/status-messages) |
|
|
92
|
+
| `prefer-aria-disabled` | HTML `disabled` removes element from tab order; `aria-disabled` keeps it discoverable | Roselli: Don't Disable Form Controls - [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
93
|
+
| `no-target-blank-without-label` | `target="_blank"` without communicating the new-tab behaviour | WebAIM - [SC 3.2.2](https://www.w3.org/WAI/WCAG21/Understanding/on-input) |
|
|
94
|
+
| `no-dialog-without-close` | `role="dialog"` or `<dialog>` without a visible close button | [APG: Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) - [SC 2.1.2](https://www.w3.org/WAI/WCAG21/Understanding/no-keyboard-trap) |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Portability rules - Vue and Angular only
|
|
99
|
+
|
|
100
|
+
These rules cover gaps in `eslint-plugin-jsx-a11y` that have no equivalent in `eslint-plugin-vuejs-accessibility` or `@angular-eslint/eslint-plugin-template`. React projects get these from jsx-a11y already.
|
|
101
|
+
|
|
102
|
+
| Rule | What it flags | Source |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| `no-anchor-ambiguous-text` | Ambiguous link text ("click here", "read more", "learn more") | [SC 2.4.4](https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context) |
|
|
105
|
+
| `no-anchor-no-content` | `<a>` with no text content and no accessible name | [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
106
|
+
| `no-aria-activedescendant-no-tabindex` | `aria-activedescendant` on an element without `tabindex` | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
|
|
107
|
+
| `no-invalid-aria-prop-value` | Invalid values on ARIA state/property attributes | [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
108
|
+
| `no-autocomplete-invalid` | Invalid `autocomplete` token values | [SC 1.3.5](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose) |
|
|
109
|
+
| `no-heading-no-content` | Headings (`<h1>`–`<h6>`) with no text content | [SC 2.4.6](https://www.w3.org/WAI/WCAG21/Understanding/headings-and-labels) |
|
|
110
|
+
| `no-iframe-no-title` | `<iframe>` without a `title` attribute | [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
111
|
+
| `no-img-redundant-alt` | Alt text containing "image", "photo", or "picture" | [SC 1.1.1](https://www.w3.org/WAI/WCAG21/Understanding/non-text-content) |
|
|
112
|
+
| `no-access-key` | `accessKey` attribute - conflicts with AT and browser shortcuts | [SC 2.1.4](https://www.w3.org/WAI/WCAG21/Understanding/character-key-shortcuts) |
|
|
113
|
+
| `no-noninteractive-to-interactive-role` | Non-interactive elements given interactive ARIA roles without keyboard handlers | [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) / [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
114
|
+
| `no-noninteractive-tabindex` | `tabindex` on a non-interactive element with no interactive role | [SC 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
|
|
115
|
+
| `prefer-semantic-element` | `<div role="button">` where a native element would be correct | [SC 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
|
|
116
|
+
| `no-role-supports-aria-props` | ARIA properties applied to roles that don't support them | [ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) |
|
|
117
|
+
| `no-scope-on-td` | `scope` attribute on `<td>` - only valid on `<th>` | [SC 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships) |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Framework-specific rules - @ulam only
|
|
122
|
+
|
|
123
|
+
These rules are specific to the @ulam framework and activate only when @ulam-related imports are detected.
|
|
124
|
+
|
|
125
|
+
| Rule | Severity | What it flags |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| `no-announce-in-render` | error | `announce()` / `clearAnnouncements()` called in a component render body or Vue setup - fires on every render, spamming screen readers. Safe contexts: `useEffect` / `onMounted` / `watch` / event handlers |
|
|
128
|
+
| `no-hash-router-in-remix` | warn | @ulam hash router alongside `react-router` - signals an incomplete Remix migration |
|
|
129
|
+
| `no-use-page-title-in-remix` | warn | `usePageTitle()` alongside `react-router` - conflicts with Remix's declarative `meta` export |
|
|
130
|
+
|
|
131
|
+
The `no-announce-in-render` rule runs in all three plugins with safe contexts per framework:
|
|
132
|
+
|
|
133
|
+
- **React:** `useEffect`, `useLayoutEffect`, `useCallback`, `useMemo`, and event handlers
|
|
134
|
+
- **Vue:** `onMounted`, `onUpdated`, `watch`, `watchEffect`, `nextTick`, and their variants
|
|
135
|
+
- **Angular:** `ngOnInit`, `ngAfterViewInit`, `ngAfterContentInit`, `ngOnChanges`, `ngDoCheck`, and class method event handlers
|
|
136
|
+
|
|
137
|
+
**Known Angular limitation:** Angular's template parser does not attach parent pointers to AST nodes. Rules that walk up the tree (`no-summary-without-details`, `no-button-type-missing`, `no-log-with-interactive-children`, `no-menu-role-on-nav`, `no-heading-inside-interactive`) will silently pass in Angular templates. The `no-dynamic-content-without-live` rule only checks the element itself (no ancestor walk) in Angular.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Rules considered and rejected
|
|
142
|
+
|
|
143
|
+
| Rule | Reason rejected |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `no-aria-controls` | Support improved substantially since Pickering's 2014 post; APG *requires* it in the tabs pattern - conflicted with `no-tabs-without-structure` |
|
|
146
|
+
| `no-aria-label-on-link` | `aria-label` on `<a>` is the correct technique for ambiguous link text; can't detect the bad case (overriding good visible text) statically |
|
|
147
|
+
| `no-aria-live-on-carousel` | Class-name heuristic - `carousel` in a class doesn't mean auto-advancing; too many false positives |
|
|
148
|
+
| `no-figure-role-without-label` | `role="figure"` on `<figure>` is redundant (element already has the role implicitly); flags the wrong thing |
|
|
149
|
+
| `no-scrollable-without-focusable` | Class-name heuristic for scroll behaviour - can't read CSS from static analysis |
|
|
150
|
+
| `no-empty-heading` | Covered by jsx-a11y recommended |
|
|
151
|
+
| `aria-required-on-required-form-control` | AT already reads native `required`; adding `aria-required` is redundant, not required |
|
|
152
|
+
| `require-menu-owned-menuitem` / `require-listbox-owned-option` | Component-based code renders children conditionally - fires constantly on empty/loading states |
|
|
153
|
+
| `no-aria-owns-circular` | Cross-file ID graph required; vanishingly rare in practice |
|
|
154
|
+
| `no-dialog-without-modal` | Non-modal dialogs are a valid APG pattern; too much false-positive risk |
|
|
155
|
+
| `no-generated-content-text` | Decorative generated content is extremely common; can't distinguish decorative from meaningful statically |
|
|
156
|
+
| DevTools console output for accessibility | Requires runtime execution; belongs in a browser devtools extension, not a static linter |
|
package/RULES.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @a11yfred/neighbor - Rule Index
|
|
2
|
+
|
|
3
|
+
Neighbor ships rules across three separate domains. Each has its own reference page.
|
|
4
|
+
|
|
5
|
+
| Domain | Entry point | Rules page |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| Markup | `@a11yfred/neighbor/eslint`, `/eslint-vue`, `/eslint-angular` | [RULES-MARKUP.md](RULES-MARKUP.md) |
|
|
8
|
+
| CSS | `@a11yfred/neighbor` (default), `@a11yfred/neighbor/stylelint` | [RULES-CSS.md](RULES-CSS.md) |
|
|
9
|
+
| Content | `@a11yfred/neighbor/content` | [RULES-CONTENT.md](RULES-CONTENT.md) |
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Markup rules - summary
|
|
14
|
+
|
|
15
|
+
ESLint rules that flag bad ARIA patterns, missing accessible names, keyboard traps, and structural errors in JSX, Vue SFCs, and Angular templates. Full reference → [RULES-MARKUP.md](RULES-MARKUP.md)
|
|
16
|
+
|
|
17
|
+
**Errors (definite breakage):** `no-aria-label-on-generic`, `no-assertive-live-overuse`, `no-unblocked-aria-disabled`, `no-roles-without-name`, `no-group-without-name`, `no-presentation-on-focusable`, `no-log-with-interactive-children`, `no-aria-hidden-in-link`, `no-redundant-aria-hidden-with-presentation`, `no-aria-owns-on-void`, `no-title-as-label`, `no-tabs-without-structure`, `no-positive-tabindex`, `no-autoplay-without-controls`, `no-heading-inside-interactive`, `no-placeholder-only`, `no-empty-button`, `no-image-role-without-name`, `no-spinbutton-without-range`, `no-slider-without-range`, `no-combobox-without-expanded`, `no-mouse-only-events`, `no-listbox-without-option`, `no-tree-without-treeitem`, `no-feed-without-article`, `no-aria-activedescendant-without-id`, `no-duplicate-id`, `no-summary-without-details`, `no-aria-required-on-non-form`, `no-input-type-invalid`, `no-labelledby-missing-target`, `no-dynamic-content-without-live`, `form-field-multiple-labels`, `no-empty-table-header`
|
|
18
|
+
|
|
19
|
+
**Warnings (on by default):** `no-tooltip-role-misuse`, `no-menu-role-on-nav`, `no-button-type-missing`
|
|
20
|
+
|
|
21
|
+
**Off by default (opt in):** `no-application-role`, `no-grid-role`, `no-aria-roledescription`, `no-aria-readonly`, `no-tab-without-controls`, `no-href-hash`, `warn-role-alert`, `prefer-aria-disabled`, `no-target-blank-without-label`, `no-dialog-without-close`
|
|
22
|
+
|
|
23
|
+
**Vue / Angular only:** `no-anchor-ambiguous-text`, `no-anchor-no-content`, `no-aria-activedescendant-no-tabindex`, `no-invalid-aria-prop-value`, `no-autocomplete-invalid`, `no-heading-no-content`, `no-iframe-no-title`, `no-img-redundant-alt`, `no-access-key`, `no-noninteractive-to-interactive-role`, `no-noninteractive-tabindex`, `prefer-semantic-element`, `no-role-supports-aria-props`, `no-scope-on-td`
|
|
24
|
+
|
|
25
|
+
**@ulam only:** `no-announce-in-render`, `no-hash-router-in-remix`, `no-use-page-title-in-remix`
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## CSS rules - summary
|
|
30
|
+
|
|
31
|
+
Stylelint rules that flag CSS that removes focus indicators, opts out of High Contrast Mode, or fails to provide user-preference media query fallbacks. Full reference → [RULES-CSS.md](RULES-CSS.md)
|
|
32
|
+
|
|
33
|
+
| Rule | What it flags |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `neighbor/user-preferences` | Animation, motion, and transparency without `@media (prefers-*)` fallbacks |
|
|
36
|
+
| `neighbor/no-outline-none` | `outline: none` outside `:focus` selectors - removes keyboard focus ring |
|
|
37
|
+
| `neighbor/no-forced-colors-none` | `forced-color-adjust: none` inside `@media (forced-colors)` - opts out of Windows High Contrast Mode |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Content rules - summary
|
|
42
|
+
|
|
43
|
+
ESLint rules that flag accessibility and inclusion problems in string literals and JSX text - ableist language, disability metaphors, English idioms, vague link and button text, directional references, unexplained abbreviations, ALL CAPS prose, and vague error messages. All ship as `warn`. Full reference → [RULES-CONTENT.md](RULES-CONTENT.md)
|
|
44
|
+
|
|
45
|
+
| Rule | What it flags | WCAG SC |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| `no-ableist-language` | Slurs, suffering-framing, condescending euphemisms ("wheelchair-bound", "suffers from", "special needs") | 3.1.1 |
|
|
48
|
+
| `no-disability-metaphor` | Disability used figuratively ("blind spot", "tone deaf", "paralyzed by") | - |
|
|
49
|
+
| `no-english-idiom` | Idioms and sports metaphors opaque to ESL readers ("slam dunk", "boil the ocean", "circle back") | 3.1.5 |
|
|
50
|
+
| `no-vague-cta` | Vague link/button text ("click here", "read more", "here") | 2.4.4 |
|
|
51
|
+
| `no-directional-language` | Position-based instructions ("see above", "in the right sidebar") | 1.3.3 |
|
|
52
|
+
| `no-unexplained-abbreviation` | Acronyms used without prior expansion in the file | 3.1.4 |
|
|
53
|
+
| `no-all-caps-prose` | ALL CAPS words that screen readers may spell out letter-by-letter | - |
|
|
54
|
+
| `no-vague-error-message` | Error messages that don't say what went wrong ("An error occurred") | 3.3.1 |
|
|
55
|
+
| `no-ampersand-in-prose` | `&` in place of "and" - announced inconsistently by screen readers | - |
|