wcag-a11y 0.4.0 → 0.4.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/README.md +133 -145
- package/dist/ai/base.js +12 -6
- package/dist/ai/fallback-fix.js +126 -0
- package/dist/ai/prompt.js +13 -6
- package/dist/cli.js +1 -1
- package/dist/reporter/markdown.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Dannyplusplus12/WCAG-A11y/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
Most accessibility auditors stop at detection — they tell you *what* is broken and leave the rest to you. `wcag-a11y`
|
|
5
|
+
Most accessibility auditors stop at detection — they tell you *what* is broken and leave the rest to you. `wcag-a11y` crawls your running dev server with Playwright, runs 40+ WCAG 2.1/2.2 checks, and uses AI to generate ready-to-paste fix prompts **or write the fixes directly into your source files**.
|
|
6
6
|
|
|
7
7
|
Two modes:
|
|
8
|
-
- **`scan`** — find violations +
|
|
8
|
+
- **`scan`** — find violations + get AI prompts you paste into Cursor, Copilot, or Claude
|
|
9
9
|
- **`fix`** — find violations + patch source files automatically (dry-run by default, `--apply` to write)
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
## Try it instantly
|
|
14
14
|
|
|
15
|
-
No dev server, no config
|
|
15
|
+
No dev server, no config:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
npx wcag-a11y demo
|
|
@@ -22,41 +22,50 @@ npx wcag-a11y demo
|
|
|
22
22
|
|
|
23
23
|
## What the output looks like
|
|
24
24
|
|
|
25
|
-
Running a scan prints a violation summary per page, then AI-generated fixes for each rule:
|
|
26
|
-
|
|
27
25
|
```
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
✖ critical
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
[
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
26
|
+
WCAG A11y — scan complete
|
|
27
|
+
────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
✖ http://localhost:3000 3 critical 4 serious 3 moderate
|
|
30
|
+
|
|
31
|
+
[CRITICAL] Images must have an alt attribute WCAG 1.1.1
|
|
32
|
+
→ img
|
|
33
|
+
[CRITICAL] Form inputs must have an associated label WCAG 1.3.1
|
|
34
|
+
→ input[type="email"]
|
|
35
|
+
[CRITICAL] Buttons must have an accessible name WCAG 4.1.2
|
|
36
|
+
→ button[type="submit"]
|
|
37
|
+
[SERIOUS] Normal text must meet 4.5:1 contrast ratio WCAG 1.4.3
|
|
38
|
+
→ p
|
|
39
|
+
[SERIOUS] Links must have descriptive text WCAG 2.4.4
|
|
40
|
+
→ a[href="/sale"]
|
|
41
|
+
… and 5 more
|
|
42
|
+
|
|
43
|
+
────────────────────────────────────────────────────────────
|
|
44
|
+
Total: 3 critical · 4 serious · 3 moderate
|
|
45
|
+
|
|
46
|
+
AI Fix Prompts — paste any of these into Cursor, Copilot, or Claude
|
|
47
|
+
────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
[img-alt]
|
|
50
|
+
→ img
|
|
51
|
+
Screen reader users hear nothing for this image — branding, instructions,
|
|
52
|
+
or data it conveys is completely invisible to them.
|
|
53
|
+
┌─ Copy this prompt ──────────────────────────────────────
|
|
54
|
+
│ Fix WCAG 1.1.1 (Level A) — img is missing an alt attribute
|
|
55
|
+
│
|
|
56
|
+
│ Current HTML:
|
|
57
|
+
│ <img src="banner.jpg">
|
|
58
|
+
│
|
|
59
|
+
│ How to fix:
|
|
60
|
+
│ Add alt text describing the image content.
|
|
61
|
+
│ Use alt="" if the image is purely decorative.
|
|
62
|
+
│ Example: <img src="banner.jpg" alt="Summer sale — 50% off">
|
|
63
|
+
└─────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
… 9 more prompts — full report saved to a11y-report.md
|
|
55
66
|
```
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
The full output is saved to `a11y-report.md` by default. Pass `--no-report` to skip.
|
|
68
|
+
Each prompt tells you the WCAG criterion, shows the broken element, and gives the exact fix — ready to paste into your AI editor.
|
|
60
69
|
|
|
61
70
|
---
|
|
62
71
|
|
|
@@ -66,61 +75,21 @@ The full output is saved to `a11y-report.md` by default. Pass `--no-report` to s
|
|
|
66
75
|
npm install -g wcag-a11y
|
|
67
76
|
```
|
|
68
77
|
|
|
69
|
-
##
|
|
78
|
+
## Quick start
|
|
70
79
|
|
|
71
80
|
```bash
|
|
72
|
-
|
|
73
|
-
wcag-a11y init
|
|
74
|
-
wcag-a11y init --provider anthropic # Anthropic Claude
|
|
75
|
-
wcag-a11y init --provider mistral # Mistral
|
|
76
|
-
wcag-a11y init --provider groq # Groq (fast inference)
|
|
77
|
-
wcag-a11y init --provider cohere # Cohere
|
|
78
|
-
wcag-a11y init --provider xai # xAI Grok
|
|
79
|
-
wcag-a11y init --provider deepseek # DeepSeek
|
|
80
|
-
wcag-a11y init --provider together # Together AI (open-source models)
|
|
81
|
-
wcag-a11y init --provider perplexity # Perplexity
|
|
82
|
-
wcag-a11y init --provider azure-openai # Azure OpenAI
|
|
83
|
-
wcag-a11y init --provider ollama # Local — no API key needed
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Each command creates an `a11y.config.json` pre-wired for that provider. Fill in your API key, then scan.
|
|
87
|
-
|
|
88
|
-
---
|
|
89
|
-
|
|
90
|
-
## Commands
|
|
81
|
+
# 1. Configure your AI provider (Gemini is free, no credit card)
|
|
82
|
+
wcag-a11y init
|
|
91
83
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
Scan a built-in page with 10 intentional violations. No dev server or config required — useful for trying the tool before pointing it at your own project.
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
wcag-a11y demo # violations + AI fixes (default, requires config)
|
|
98
|
-
wcag-a11y demo --no-ai # violations only, no AI — faster
|
|
99
|
-
wcag-a11y demo --no-report # skip saving a11y-report.md
|
|
84
|
+
# 2. Start your dev server, then scan
|
|
85
|
+
wcag-a11y scan -u http://localhost:3000
|
|
100
86
|
```
|
|
101
87
|
|
|
102
|
-
|
|
103
|
-
|---|---|
|
|
104
|
-
| `--no-ai` | Skip AI fix generation — prints violations only, no prompts |
|
|
105
|
-
| `--no-report` | Skip saving report to `a11y-report.md` (report is saved by default) |
|
|
88
|
+
Add `--pages / /about /contact` to scan specific routes, or `--crawl` to follow links automatically.
|
|
106
89
|
|
|
107
90
|
---
|
|
108
91
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
Create `a11y.config.json` in the current directory, pre-configured for your chosen provider.
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
wcag-a11y init # Gemini (default)
|
|
115
|
-
wcag-a11y init --provider openai # OpenAI
|
|
116
|
-
wcag-a11y init --provider ollama # Ollama (local)
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
| Flag | Description |
|
|
120
|
-
|---|---|
|
|
121
|
-
| `--provider <name>` | Which provider to configure. Valid values: `gemini` (default), `openai`, `anthropic`, `mistral`, `groq`, `cohere`, `xai`, `deepseek`, `together`, `perplexity`, `azure-openai`, `ollama`. Determines which fields are written to the config file. |
|
|
122
|
-
|
|
123
|
-
---
|
|
92
|
+
## Commands
|
|
124
93
|
|
|
125
94
|
### `wcag-a11y scan`
|
|
126
95
|
|
|
@@ -137,16 +106,16 @@ wcag-a11y scan -u http://localhost:3000 --terminal --fast-mode
|
|
|
137
106
|
| Flag | Default | Description |
|
|
138
107
|
|---|---|---|
|
|
139
108
|
| `-u, --url <url>` | required | Base URL of your running dev server |
|
|
140
|
-
| `-p, --pages <
|
|
141
|
-
| `-c, --crawl` | off | Follow same-origin links and scan all reachable pages
|
|
142
|
-
| `--no-
|
|
143
|
-
| `--no-
|
|
144
|
-
| `--no-explain` |
|
|
145
|
-
| `--terminal` | off | Print violations
|
|
146
|
-
| `--fast-mode` | off | Output only
|
|
147
|
-
| `--group <strategy>` | `rule` | `rule
|
|
148
|
-
| `--ci` | off | Exit with code `1` if any violations are found
|
|
149
|
-
| `--provider <name>` | from config | Override
|
|
109
|
+
| `-p, --pages <paths...>` | `/` | Paths to scan. Space-separated: `--pages / /about /contact` |
|
|
110
|
+
| `-c, --crawl` | off | Follow same-origin links and scan all reachable pages |
|
|
111
|
+
| `--no-ai` | — | Skip AI fix generation — scan runs faster, violations only |
|
|
112
|
+
| `--no-report` | — | Skip saving `a11y-report.md` |
|
|
113
|
+
| `--no-explain` | — | Omit explanations, show prompts only |
|
|
114
|
+
| `--terminal` | off | Print violations and AI prompts to terminal |
|
|
115
|
+
| `--fast-mode` | off | Output only the raw prompts — no summaries or decoration |
|
|
116
|
+
| `--group <strategy>` | `rule` | `rule`: one prompt per rule type. `none`: one prompt per element |
|
|
117
|
+
| `--ci` | off | Exit with code `1` if any violations are found |
|
|
118
|
+
| `--provider <name>` | from config | Override AI provider for this run |
|
|
150
119
|
|
|
151
120
|
---
|
|
152
121
|
|
|
@@ -155,34 +124,13 @@ wcag-a11y scan -u http://localhost:3000 --terminal --fast-mode
|
|
|
155
124
|
Scan for violations and apply AI-generated patches directly to your source files. Works with any framework — React, Vue, Angular, Svelte, or plain HTML.
|
|
156
125
|
|
|
157
126
|
```bash
|
|
158
|
-
#
|
|
159
|
-
wcag-a11y fix -u http://localhost:3000
|
|
160
|
-
|
|
161
|
-
# Preview specific pages
|
|
162
|
-
wcag-a11y fix -u http://localhost:3000 --pages / /about /contact
|
|
163
|
-
|
|
164
|
-
# Write fixes to disk
|
|
165
|
-
wcag-a11y fix -u http://localhost:3000 --apply
|
|
166
|
-
|
|
167
|
-
# Auto-discover pages + write fixes
|
|
168
|
-
wcag-a11y fix -u http://localhost:3000 --crawl --apply
|
|
169
|
-
|
|
170
|
-
# Skip rescanning — load violations from an existing report
|
|
171
|
-
wcag-a11y fix --from-report
|
|
172
|
-
wcag-a11y fix --from-report ./reports/a11y-report.md --apply
|
|
127
|
+
wcag-a11y fix -u http://localhost:3000 # dry-run: show diff, nothing written
|
|
128
|
+
wcag-a11y fix -u http://localhost:3000 --apply # write fixes to disk
|
|
129
|
+
wcag-a11y fix --from-report --apply # patch from an existing report
|
|
173
130
|
```
|
|
174
131
|
|
|
175
|
-
**How it works:**
|
|
176
|
-
|
|
177
|
-
1. Runs the same scan as `wcag-a11y scan` (or loads an existing report with `--from-report`)
|
|
178
|
-
2. For each violation, locates the source file — checks `violation.source` (React dev mode) first, then falls back to grepping `./src` for unique identifiers in the HTML snippet (`id=`, `name=`, `for=`, local `src=`, text content)
|
|
179
|
-
3. Groups violations by file (multiple violations in the same file → one AI call)
|
|
180
|
-
4. Sends the full file content + violation list to your configured AI provider and asks for the corrected file
|
|
181
|
-
5. Shows a colored diff before writing anything
|
|
182
|
-
6. With `--apply`, overwrites the file; without it, only prints the diff
|
|
183
|
-
|
|
184
132
|
```
|
|
185
|
-
src/components/Navbar.jsx — 2
|
|
133
|
+
src/components/Navbar.jsx — 2 violations
|
|
186
134
|
· [button-name] Buttons must have an accessible name
|
|
187
135
|
· [aria-valid-role] Elements must use valid ARIA roles
|
|
188
136
|
|
|
@@ -196,52 +144,85 @@ src/components/Navbar.jsx — 2 violation(s)
|
|
|
196
144
|
+ <li>Home</li>
|
|
197
145
|
```
|
|
198
146
|
|
|
147
|
+
**Common workflow:** scan first to review, then patch:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
wcag-a11y scan -u http://localhost:3000 # generates a11y-report.md
|
|
151
|
+
wcag-a11y fix --from-report --apply # patches files from that report, no second crawl
|
|
152
|
+
```
|
|
153
|
+
|
|
199
154
|
| Flag | Default | Description |
|
|
200
155
|
|---|---|---|
|
|
201
|
-
| `-u, --url <url>` | — | Base URL
|
|
202
|
-
| `-p, --pages <
|
|
156
|
+
| `-u, --url <url>` | — | Base URL. Required unless `--from-report` is used |
|
|
157
|
+
| `-p, --pages <paths...>` | `/` | Paths to scan |
|
|
203
158
|
| `-c, --crawl` | off | Auto-discover pages by following same-origin links |
|
|
204
|
-
| `--from-report [path]` | `a11y-report.md` | Load violations from an existing report instead of
|
|
205
|
-
| `--apply` | off | Write
|
|
206
|
-
| `--provider <name>` | from config | Override AI provider for this run
|
|
159
|
+
| `--from-report [path]` | `a11y-report.md` | Load violations from an existing report instead of rescanning |
|
|
160
|
+
| `--apply` | off | Write fixes to disk (dry-run without this flag) |
|
|
161
|
+
| `--provider <name>` | from config | Override AI provider for this run |
|
|
162
|
+
|
|
163
|
+
---
|
|
207
164
|
|
|
208
|
-
|
|
165
|
+
### `wcag-a11y init`
|
|
209
166
|
|
|
210
|
-
|
|
167
|
+
Create `a11y.config.json` pre-configured for your chosen provider.
|
|
211
168
|
|
|
212
169
|
```bash
|
|
213
|
-
wcag-a11y
|
|
214
|
-
wcag-a11y
|
|
170
|
+
wcag-a11y init # Gemini (free, default)
|
|
171
|
+
wcag-a11y init --provider openai
|
|
172
|
+
wcag-a11y init --provider anthropic
|
|
173
|
+
wcag-a11y init --provider ollama # local — no API key needed
|
|
174
|
+
# … and 8 more providers
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### `wcag-a11y demo`
|
|
180
|
+
|
|
181
|
+
Scan a built-in page with 10 intentional violations. No dev server or config required — useful for trying the tool before pointing it at your own project.
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
wcag-a11y demo # violations + AI fix prompts (requires config)
|
|
185
|
+
wcag-a11y demo --no-ai # violations only, no AI
|
|
215
186
|
```
|
|
216
187
|
|
|
217
188
|
---
|
|
218
189
|
|
|
219
190
|
## AI Providers
|
|
220
191
|
|
|
221
|
-
12 providers
|
|
192
|
+
12 providers supported. Configure once in `a11y.config.json`, or override per-run with `--provider`.
|
|
222
193
|
|
|
223
|
-
| Provider | `--provider`
|
|
194
|
+
| Provider | `--provider` | Default model | Notes |
|
|
224
195
|
|---|---|---|---|
|
|
225
|
-
| Google Gemini | `gemini` *(default)* | `gemini-2.5-flash` |
|
|
226
|
-
| OpenAI | `openai` | `gpt-4o-mini` |
|
|
227
|
-
| Anthropic | `anthropic` | `claude-sonnet-4-6` |
|
|
228
|
-
| Mistral | `mistral` | `mistral-large-latest` |
|
|
229
|
-
| Groq | `groq` | `llama-3.3-70b-versatile` |
|
|
230
|
-
| Cohere | `cohere` | `command-r-plus` |
|
|
231
|
-
| xAI | `xai` | `grok-2` |
|
|
232
|
-
| DeepSeek | `deepseek` | `deepseek-chat` |
|
|
233
|
-
| Together AI | `together` | `meta-llama/Llama-3-70b-chat-hf` |
|
|
234
|
-
| Perplexity | `perplexity` | `llama-3.1-sonar-large-128k-online` |
|
|
235
|
-
| Azure OpenAI | `azure-openai` | *(your deployment)* |
|
|
236
|
-
| Ollama | `ollama` | `llama3` |
|
|
237
|
-
|
|
238
|
-
All models are configurable.
|
|
196
|
+
| Google Gemini | `gemini` *(default)* | `gemini-2.5-flash` | Free tier available |
|
|
197
|
+
| OpenAI | `openai` | `gpt-4o-mini` | |
|
|
198
|
+
| Anthropic | `anthropic` | `claude-sonnet-4-6` | |
|
|
199
|
+
| Mistral | `mistral` | `mistral-large-latest` | |
|
|
200
|
+
| Groq | `groq` | `llama-3.3-70b-versatile` | Fast inference |
|
|
201
|
+
| Cohere | `cohere` | `command-r-plus` | |
|
|
202
|
+
| xAI | `xai` | `grok-2` | |
|
|
203
|
+
| DeepSeek | `deepseek` | `deepseek-chat` | |
|
|
204
|
+
| Together AI | `together` | `meta-llama/Llama-3-70b-chat-hf` | Open-source models |
|
|
205
|
+
| Perplexity | `perplexity` | `llama-3.1-sonar-large-128k-online` | |
|
|
206
|
+
| Azure OpenAI | `azure-openai` | *(your deployment)* | |
|
|
207
|
+
| Ollama | `ollama` | `llama3` | Local — no API key |
|
|
208
|
+
|
|
209
|
+
All models are configurable. If the AI response is unparseable, the tool generates a fix prompt directly from the violation data — you always get something actionable.
|
|
239
210
|
|
|
240
211
|
---
|
|
241
212
|
|
|
242
|
-
## Config
|
|
213
|
+
## Config
|
|
243
214
|
|
|
244
|
-
Only the fields for your
|
|
215
|
+
Run `wcag-a11y init` to generate `a11y.config.json`. Only fill in the fields for your chosen provider. This file is gitignored by default.
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"provider": "gemini",
|
|
220
|
+
"apiKey": "YOUR_GEMINI_API_KEY"
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
<details>
|
|
225
|
+
<summary>Full config reference (all 12 providers)</summary>
|
|
245
226
|
|
|
246
227
|
```json
|
|
247
228
|
{
|
|
@@ -287,11 +268,16 @@ Only the fields for your active `provider` are required. This file is gitignored
|
|
|
287
268
|
}
|
|
288
269
|
```
|
|
289
270
|
|
|
271
|
+
</details>
|
|
272
|
+
|
|
290
273
|
---
|
|
291
274
|
|
|
292
275
|
## What it checks
|
|
293
276
|
|
|
294
|
-
40+ rules across 10
|
|
277
|
+
40+ rules across 10 WCAG 2.1/2.2 categories: Text Alternatives, Color Contrast, Forms, Keyboard, ARIA, Structure, Links, Media, Tables, and Language.
|
|
278
|
+
|
|
279
|
+
<details>
|
|
280
|
+
<summary>Full rule list</summary>
|
|
295
281
|
|
|
296
282
|
### Text Alternatives — WCAG 1.1.1
|
|
297
283
|
|
|
@@ -394,6 +380,8 @@ Only the fields for your active `provider` are required. This file is gitignored
|
|
|
394
380
|
| `html-lang` | serious | `<html>` must have a `lang` attribute |
|
|
395
381
|
| `html-lang-valid` | serious | `lang` attribute must be a valid BCP 47 language tag |
|
|
396
382
|
|
|
383
|
+
</details>
|
|
384
|
+
|
|
397
385
|
---
|
|
398
386
|
|
|
399
387
|
## Use in CI/CD
|
package/dist/ai/base.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fallbackExplanation } from './fallback-explanation.js';
|
|
2
|
+
import { getFix, getFixCategory } from './fallback-fix.js';
|
|
2
3
|
export class BaseAIProvider {
|
|
3
4
|
parse(text, groups, framework) {
|
|
4
5
|
try {
|
|
@@ -7,7 +8,7 @@ export class BaseAIProvider {
|
|
|
7
8
|
return groups.map((g) => {
|
|
8
9
|
const found = fixes.find((f) => f.ruleId === g.ruleId);
|
|
9
10
|
return found
|
|
10
|
-
? { ...found, selectors: g.selectors, instanceCount: g.count }
|
|
11
|
+
? { ...found, selectors: g.selectors, instanceCount: g.count, fixCategory: getFixCategory(g.ruleId) }
|
|
11
12
|
: this.fallbackFix(g, framework);
|
|
12
13
|
});
|
|
13
14
|
}
|
|
@@ -20,18 +21,23 @@ export class BaseAIProvider {
|
|
|
20
21
|
}
|
|
21
22
|
fallbackFix(g, framework) {
|
|
22
23
|
const v = g.representative;
|
|
23
|
-
const selectorList = g.selectors.map((s) => `-
|
|
24
|
+
const selectorList = g.selectors.map((s) => `- \`${s}\``).join('\n');
|
|
24
25
|
const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
|
|
25
|
-
const fwNote = framework ? `This project uses ${framework}
|
|
26
|
+
const fwNote = framework ? `This project uses ${framework}.\n\n` : '';
|
|
27
|
+
const fixInstructions = getFix(g.ruleId);
|
|
28
|
+
const fixSection = fixInstructions ? `\n\nHow to fix:\n${fixInstructions}` : '';
|
|
26
29
|
const prompt = g.count > 1
|
|
27
|
-
? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n
|
|
28
|
-
: `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n
|
|
30
|
+
? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`\`\`html\n${v.html.slice(0, 400)}\n\`\`\`${fixSection}\n\nApply this fix to all ${g.count} instances across the codebase.`
|
|
31
|
+
: `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n\nCurrent HTML:\n\`\`\`html\n${v.html.slice(0, 400)}\n\`\`\`${fixSection}`;
|
|
32
|
+
const category = getFixCategory(g.ruleId);
|
|
33
|
+
const fixedCode = category === 'edit-element' || !category ? v.html : undefined;
|
|
29
34
|
return {
|
|
30
35
|
ruleId: g.ruleId,
|
|
31
36
|
selectors: g.selectors,
|
|
32
37
|
instanceCount: g.count,
|
|
33
38
|
explanation,
|
|
34
|
-
fixedCode
|
|
39
|
+
fixedCode,
|
|
40
|
+
fixCategory: getFixCategory(g.ruleId),
|
|
35
41
|
wcagReference: `WCAG 2.1 SC ${g.wcag}`,
|
|
36
42
|
optimalPrompt: prompt,
|
|
37
43
|
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const RULE_FIX_INSTRUCTIONS = {
|
|
2
|
+
// Text alternatives
|
|
3
|
+
'img-alt': 'Add an `alt` attribute describing the image content. Use `alt=""` for decorative images.\nExample: `<img src="photo.jpg" alt="Team photo at the 2024 company retreat">`',
|
|
4
|
+
'input-image-alt': 'Add an `alt` attribute to the `<input type="image">` describing the button\'s action.\nExample: `<input type="image" src="submit.png" alt="Submit form">`',
|
|
5
|
+
'svg-title': 'Add a `<title>` element as the first child of `<svg>` and add `aria-labelledby` pointing to its `id`.\nExample: `<svg aria-labelledby="chartTitle" role="img"><title id="chartTitle">Monthly sales bar chart</title>...</svg>`',
|
|
6
|
+
'object-alt': 'Add descriptive fallback content inside the `<object>` tag, or use `aria-label`.\nExample: `<object data="report.pdf"><p>Download the Q4 2024 report (PDF)</p></object>`',
|
|
7
|
+
'role-img-alt': 'Add `aria-label` or `aria-labelledby` to the element with `role="img"`.\nExample: `<div role="img" aria-label="Star rating: 4 out of 5"></div>`',
|
|
8
|
+
'image-redundant-alt': 'Change the `alt` to `alt=""` (empty string) since the image\'s meaning is already conveyed by adjacent text — screen readers skip empty-alt images automatically.',
|
|
9
|
+
// Tables
|
|
10
|
+
'table-headers': 'Add `<th scope="col">` for column headers and `<th scope="row">` for row headers.\nExample: `<thead><tr><th scope="col">Name</th><th scope="col">Price</th></tr></thead>`',
|
|
11
|
+
'table-scope-valid': 'Change the `scope` attribute to a valid value: `"col"`, `"row"`, `"colgroup"`, or `"rowgroup"`. Remove it if the element is not a header.',
|
|
12
|
+
'td-headers-attr': 'Either add matching `id` attributes to the referenced `<th>` elements, or remove the invalid `headers` attributes from the `<td>` elements.',
|
|
13
|
+
'table-duplicate-name': 'Make the `<caption>` and `summary` convey different information: use `<caption>` for a short visible title and `summary` for an extended description, or remove the redundant attribute.',
|
|
14
|
+
// Structure
|
|
15
|
+
'heading-order': 'Restructure headings so levels are never skipped. Change the offending tag to the correct level (e.g., change `<h4>` directly under `<h2>` to `<h3>`). Use CSS for visual sizing — never pick heading levels for appearance.',
|
|
16
|
+
'page-title': 'Add or update `<title>` in `<head>` with a unique, descriptive title.\nUse the format: `<title>Page Name — Site Name</title>`',
|
|
17
|
+
'landmark-one-main': 'Wrap primary page content in exactly one `<main id="main-content">` element. Remove any duplicate `<main>` elements.',
|
|
18
|
+
'list-structure': 'Replace non-semantic markup with proper list structure.\nExample: change `<div class="item">` repeated items into `<ul><li>...</li><li>...</li></ul>` for unordered, or `<ol>` for ordered lists.',
|
|
19
|
+
'region-landmark': 'Wrap the content in the appropriate landmark element: `<header>`, `<nav aria-label="...">`, `<main>`, `<aside aria-label="...">`, or `<footer>`. For generic sections: `<section aria-label="Section name">`.',
|
|
20
|
+
'duplicate-id': 'Make each `id` unique across the page. If the ID is referenced by `aria-labelledby` or `aria-describedby`, update those references to match the new unique IDs. Append a suffix like `-nav`, `-footer`, or a number to disambiguate.',
|
|
21
|
+
'frame-title': 'Add a `title` attribute to the `<iframe>` describing its content.\nExample: `<iframe title="Product demo video" src="..."></iframe>`',
|
|
22
|
+
'meta-viewport': 'Remove `user-scalable=no` and ensure `maximum-scale` is not below 5.\nChange to: `<meta name="viewport" content="width=device-width, initial-scale=1">`',
|
|
23
|
+
'marquee': 'Replace `<marquee>` with a CSS-animated element. Respect `prefers-reduced-motion`:\n`@media (prefers-reduced-motion: reduce) { .marquee { animation: none; } }`\nProvide static fallback content for motion-sensitive users.',
|
|
24
|
+
'p-as-heading': 'Replace the visually bold `<p>` with the appropriate heading tag (`<h2>`, `<h3>`, etc.) matching its position in the document outline. Apply visual styles via CSS, not `<b>` or `<strong>` tags inside `<p>`.',
|
|
25
|
+
// Media
|
|
26
|
+
'video-captions': 'Add a `<track kind="captions">` element inside the `<video>` pointing to a WebVTT (.vtt) file.\nExample: `<track kind="captions" src="/captions-en.vtt" srclang="en" label="English" default>`',
|
|
27
|
+
'audio-description': 'Add a `<track kind="descriptions">` element inside `<video>` for audio descriptions.\nExample: `<track kind="descriptions" src="/descriptions-en.vtt" srclang="en" label="Audio description">`',
|
|
28
|
+
'audio-transcript': 'Provide a text transcript immediately after the `<audio>` element, either as a link or inline.\nExample: `<a href="/transcript.html">Read transcript</a>`',
|
|
29
|
+
// Links
|
|
30
|
+
'link-name': 'Add descriptive text inside the `<a>`, or use `aria-label` to describe the destination.\nExample: `<a href="/products" aria-label="View all products">View all</a>`. Avoid "click here" or "read more".',
|
|
31
|
+
'link-empty': 'Add descriptive text or `aria-label` to the empty anchor. If it is a decorative icon, add `aria-label`. If non-functional, remove the `<a>` element entirely.',
|
|
32
|
+
'identical-links-different-purpose': 'Add unique `aria-label` to each link to distinguish its destination.\nExample: `<a href="/product-a" aria-label="Read more about Product A">Read more</a>` and `<a href="/product-b" aria-label="Read more about Product B">Read more</a>`',
|
|
33
|
+
'link-new-window-warn': 'Add `rel="noopener noreferrer"` and warn users the link opens a new tab.\nExample: `<a href="..." target="_blank" rel="noopener noreferrer">Download PDF <span class="sr-only">(opens in new tab)</span></a>`',
|
|
34
|
+
// Forms
|
|
35
|
+
'label-missing': 'Add a `<label>` linked via `for`/`id`, or add `aria-label` directly to the input.\nExample: `<label for="user-email">Email address</label><input id="user-email" type="email">`',
|
|
36
|
+
'label-empty': 'Add descriptive text inside the existing `<label>`.\nExample: change `<label for="q"></label>` to `<label for="q">Search</label>`',
|
|
37
|
+
'error-identification': 'Link error messages to their fields using `aria-describedby` and add `role="alert"` to the error container.\nExample: `<input aria-describedby="email-error"><span id="email-error" role="alert">Please enter a valid email address</span>`',
|
|
38
|
+
'autocomplete': 'Add the correct `autocomplete` attribute to the input. Common values: `name`, `email`, `tel`, `current-password`, `new-password`, `given-name`, `family-name`, `street-address`, `postal-code`.\nExample: `<input type="email" autocomplete="email">`',
|
|
39
|
+
'input-button-name': 'Add a descriptive `value` to `<input type="button/submit/reset">` or `aria-label` to icon buttons.\nExample: `<input type="submit" value="Submit registration form">` or `<button aria-label="Search"><svg aria-hidden="true">...</svg></button>`',
|
|
40
|
+
'fieldset-legend': 'Wrap related controls in `<fieldset>` with `<legend>` as its first child.\nExample: `<fieldset><legend>Shipping address</legend><label>...</label><input>...</fieldset>`',
|
|
41
|
+
'form-field-required-label': 'Add `required` and `aria-required="true"` to the input. Indicate required status in the label text.\nExample: `<label for="name">Full name <span aria-hidden="true">*</span><span class="sr-only">(required)</span></label><input id="name" required aria-required="true">`',
|
|
42
|
+
// Language
|
|
43
|
+
'html-lang': 'Add a `lang` attribute to the `<html>` element with the correct BCP 47 language tag.\nExample: `<html lang="en">` for English, `<html lang="vi">` for Vietnamese, `<html lang="fr">` for French.',
|
|
44
|
+
'html-lang-valid': 'Replace the invalid `lang` value with a valid BCP 47 language tag.\nValid examples: `"en"`, `"en-US"`, `"fr"`, `"de"`, `"es"`, `"zh"`, `"ja"`, `"ko"`, `"vi"`, `"ar"`.',
|
|
45
|
+
// Color contrast
|
|
46
|
+
'color-contrast-text': 'Increase the contrast ratio between the text and background to at least 4.5:1. Darken the text color or lighten the background. Verify with the WebAIM Contrast Checker.\nExample: change `color: #999` on a white background to `color: #767676` (minimum passing value).',
|
|
47
|
+
'color-contrast-large-text': 'Increase the contrast ratio to at least 3:1 for large text (18pt/24px+ regular, or 14pt/18.67px+ bold). Adjust the text or background color and verify with a contrast checker.',
|
|
48
|
+
// Keyboard
|
|
49
|
+
'no-positive-tabindex': 'Remove the `tabindex` attribute or set it to `tabindex="0"`. Reorder DOM elements to create the correct focus sequence — never use positive tabindex values to manage tab order.',
|
|
50
|
+
'interactive-not-focusable': 'Add `role="button"` (or the correct role) and `tabindex="0"`. Add keyboard event handlers for Enter and Space to match the click handler.\nExample: `<div role="button" tabindex="0" onclick="handleClick()" onkeydown="if(event.key===\'Enter\'||event.key===\' \')handleClick()">`',
|
|
51
|
+
'skip-link': 'Add a skip link as the very first child of `<body>`:\n`<a href="#main-content" class="skip-link">Skip to main content</a>`\nEnsure the target exists: `<main id="main-content">` (add `id="main-content"` to your existing `<main>` element).\nAdd CSS: `.skip-link { position: absolute; left: -9999px; } .skip-link:focus { position: static; left: 0; top: 0; z-index: 9999; padding: 8px; background: #fff; color: #000; }`',
|
|
52
|
+
'focus-visible': 'Remove `outline: none` or `outline: 0` from `:focus` styles. Add a clearly visible focus indicator.\nExample: `a:focus-visible, button:focus-visible { outline: 3px solid #005fcc; outline-offset: 2px; }`. Never suppress focus outlines without an equally visible replacement.',
|
|
53
|
+
'scrollable-region-focusable': 'Add `tabindex="0"` to the scrollable container so keyboard users can focus and scroll it.\nExample: `<div class="overflow-auto" tabindex="0" aria-label="Scrollable content">...</div>`',
|
|
54
|
+
'accesskey-unique': 'Change duplicate `accesskey` values so each one is unique across the page. If shortcuts are not needed, remove the `accesskey` attribute entirely.',
|
|
55
|
+
// ARIA
|
|
56
|
+
'aria-valid-role': 'Remove the invalid `role` value or replace it with a valid WAI-ARIA role such as `button`, `checkbox`, `dialog`, `listbox`, `menu`, `menuitem`, `option`, `radio`, `tab`, `tabpanel`, or `tooltip`.',
|
|
57
|
+
'aria-required-attr': 'Add the missing required ARIA state or property for this role.\nExamples: `role="checkbox"` needs `aria-checked`; `role="combobox"` needs `aria-expanded`; `role="slider"` needs `aria-valuenow`, `aria-valuemin`, and `aria-valuemax`.',
|
|
58
|
+
'aria-hidden-focus': 'Remove `aria-hidden="true"` from any ancestor of a focusable element, or move `aria-hidden` to a sibling that contains no focusable descendants. Never place interactive elements inside `aria-hidden="true"` containers.',
|
|
59
|
+
'button-name': 'Add visible text content or `aria-label` to the `<button>`.\nExample: `<button aria-label="Close dialog"><svg aria-hidden="true">...</svg></button>` or `<button>Submit order</button>`',
|
|
60
|
+
'aria-required-children': 'Add the required child elements with the correct ARIA roles inside this container.\nExamples: `role="listbox"` must contain `role="option"` children; `role="menu"` must contain `role="menuitem"` children; `role="grid"` must contain `role="row"` children.',
|
|
61
|
+
'aria-required-parent': 'Move this element inside its required ARIA parent container.\nExamples: `role="option"` must be inside `role="listbox"`; `role="tab"` must be inside `role="tablist"`; `role="menuitem"` must be inside `role="menu"`.',
|
|
62
|
+
'aria-prohibited-attr': 'Remove the prohibited ARIA attribute from this element. Elements with `role="presentation"` or `role="none"` must not have naming attributes like `aria-label` or `aria-labelledby`. Check the WAI-ARIA spec for the element\'s prohibited attributes.',
|
|
63
|
+
};
|
|
64
|
+
export function getFix(ruleId) {
|
|
65
|
+
return RULE_FIX_INSTRUCTIONS[ruleId];
|
|
66
|
+
}
|
|
67
|
+
const RULE_FIX_CATEGORY = {
|
|
68
|
+
// edit-element — fix targets the flagged element's own attributes or content
|
|
69
|
+
'img-alt': 'edit-element',
|
|
70
|
+
'input-image-alt': 'edit-element',
|
|
71
|
+
'svg-title': 'edit-element',
|
|
72
|
+
'object-alt': 'edit-element',
|
|
73
|
+
'role-img-alt': 'edit-element',
|
|
74
|
+
'image-redundant-alt': 'edit-element',
|
|
75
|
+
'table-scope-valid': 'edit-element',
|
|
76
|
+
'table-duplicate-name': 'edit-element',
|
|
77
|
+
'page-title': 'edit-element',
|
|
78
|
+
'frame-title': 'edit-element',
|
|
79
|
+
'meta-viewport': 'edit-element',
|
|
80
|
+
'p-as-heading': 'edit-element',
|
|
81
|
+
'heading-order': 'edit-element',
|
|
82
|
+
'duplicate-id': 'edit-element',
|
|
83
|
+
'link-name': 'edit-element',
|
|
84
|
+
'link-empty': 'edit-element',
|
|
85
|
+
'identical-links-different-purpose': 'edit-element',
|
|
86
|
+
'link-new-window-warn': 'edit-element',
|
|
87
|
+
'label-empty': 'edit-element',
|
|
88
|
+
'autocomplete': 'edit-element',
|
|
89
|
+
'input-button-name': 'edit-element',
|
|
90
|
+
'html-lang': 'edit-element',
|
|
91
|
+
'html-lang-valid': 'edit-element',
|
|
92
|
+
'no-positive-tabindex': 'edit-element',
|
|
93
|
+
'interactive-not-focusable': 'edit-element',
|
|
94
|
+
'scrollable-region-focusable': 'edit-element',
|
|
95
|
+
'accesskey-unique': 'edit-element',
|
|
96
|
+
'aria-valid-role': 'edit-element',
|
|
97
|
+
'aria-required-attr': 'edit-element',
|
|
98
|
+
'aria-prohibited-attr': 'edit-element',
|
|
99
|
+
'button-name': 'edit-element',
|
|
100
|
+
'aria-required-children': 'edit-element',
|
|
101
|
+
'video-captions': 'edit-element',
|
|
102
|
+
'audio-description': 'edit-element',
|
|
103
|
+
// add-elsewhere — fix inserts a new element at a different location
|
|
104
|
+
'skip-link': 'add-elsewhere',
|
|
105
|
+
'audio-transcript': 'add-elsewhere',
|
|
106
|
+
'label-missing': 'add-elsewhere',
|
|
107
|
+
'error-identification': 'add-elsewhere',
|
|
108
|
+
// change-css — fix lives in a stylesheet, not in the flagged element's markup
|
|
109
|
+
'color-contrast-text': 'change-css',
|
|
110
|
+
'color-contrast-large-text': 'change-css',
|
|
111
|
+
'focus-visible': 'change-css',
|
|
112
|
+
'marquee': 'change-css',
|
|
113
|
+
// restructure — fix touches multiple elements or wraps / moves content
|
|
114
|
+
'table-headers': 'restructure',
|
|
115
|
+
'td-headers-attr': 'restructure',
|
|
116
|
+
'landmark-one-main': 'restructure',
|
|
117
|
+
'list-structure': 'restructure',
|
|
118
|
+
'region-landmark': 'restructure',
|
|
119
|
+
'fieldset-legend': 'restructure',
|
|
120
|
+
'form-field-required-label': 'restructure',
|
|
121
|
+
'aria-hidden-focus': 'restructure',
|
|
122
|
+
'aria-required-parent': 'restructure',
|
|
123
|
+
};
|
|
124
|
+
export function getFixCategory(ruleId) {
|
|
125
|
+
return RULE_FIX_CATEGORY[ruleId];
|
|
126
|
+
}
|
package/dist/ai/prompt.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getFix, getFixCategory } from './fallback-fix.js';
|
|
1
2
|
const FRAMEWORK_SYNTAX = {
|
|
2
3
|
'Next.js': 'React/TSX (JSX)',
|
|
3
4
|
'Gatsby': 'React/JSX',
|
|
@@ -12,22 +13,28 @@ const FRAMEWORK_SYNTAX = {
|
|
|
12
13
|
export function buildPrompt(groups, framework) {
|
|
13
14
|
const syntax = framework ? (FRAMEWORK_SYNTAX[framework] ?? 'JSX/TSX') : null;
|
|
14
15
|
const items = groups
|
|
15
|
-
.map((g, i) =>
|
|
16
|
+
.map((g, i) => {
|
|
17
|
+
const fixHint = getFix(g.ruleId);
|
|
18
|
+
const category = getFixCategory(g.ruleId) ?? 'edit-element';
|
|
19
|
+
const fixLine = fixHint ? `\n Fix guidance: ${fixHint}` : '';
|
|
20
|
+
const categoryLine = `\n Fix type: ${category}`;
|
|
21
|
+
return `${i + 1}. Rule: ${g.ruleId} | WCAG ${g.wcag} (Level ${g.level}) | Impact: ${g.impact}
|
|
16
22
|
Page: ${g.page}
|
|
17
23
|
Instances: ${g.count} element(s)
|
|
18
24
|
Selectors: ${g.selectors.join(', ')}
|
|
19
25
|
Representative Element: ${g.representative.html}
|
|
20
|
-
Problem: ${g.description}
|
|
26
|
+
Problem: ${g.description}${categoryLine}${fixLine}`;
|
|
27
|
+
})
|
|
21
28
|
.join('\n\n');
|
|
22
29
|
const frameworkLine = framework
|
|
23
30
|
? `\nFramework: ${framework}. All "fixedCode" values must use ${syntax} syntax — not raw HTML.\n`
|
|
24
31
|
: '';
|
|
25
32
|
const fixedCodeInstruction = syntax
|
|
26
|
-
? `"fixedCode": the
|
|
27
|
-
: `"fixedCode": the corrected HTML snippet
|
|
33
|
+
? `"fixedCode": include ONLY when the violation's "Fix type" is "edit-element". Show the corrected ${syntax} snippet — NOT a copy of the original broken element. Omit this field entirely for "add-elsewhere", "change-css", and "restructure" violations.`
|
|
34
|
+
: `"fixedCode": include ONLY when the violation's "Fix type" is "edit-element". Show the corrected HTML snippet — NOT a copy of the original broken element. Omit this field entirely for "add-elsewhere", "change-css", and "restructure" violations.`;
|
|
28
35
|
const optimalPromptInstruction = framework
|
|
29
|
-
? `"optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude) working in a ${framework} codebase. Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated, (2)
|
|
30
|
-
: `"optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude). Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated, (2)
|
|
36
|
+
? `"optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude) working in a ${framework} codebase. Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated, (2) quote the affected selector and current HTML, (3) provide the exact code change required in ${syntax} syntax — include a concrete before/after snippet or the specific element to add/modify/remove. The prompt must be actionable without requiring additional research.`
|
|
37
|
+
: `"optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude). Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated, (2) quote the affected selector and current HTML, (3) provide the exact code change required — include a concrete before/after snippet or the specific element to add/modify/remove. The prompt must be fully actionable: tell the developer exactly what to write, not just that something needs fixing.`;
|
|
31
38
|
return `You are a WCAG accessibility expert. Analyze these violations and return a JSON array.${frameworkLine}
|
|
32
39
|
Each item must have:
|
|
33
40
|
- "ruleId": the rule id from the input
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ const program = new Command();
|
|
|
13
13
|
program
|
|
14
14
|
.name('wcag-a11y')
|
|
15
15
|
.description('WCAG 2.1/2.2 accessibility auditor with AI-powered fixes')
|
|
16
|
-
.version('0.4.
|
|
16
|
+
.version('0.4.1');
|
|
17
17
|
program
|
|
18
18
|
.command('init')
|
|
19
19
|
.description('Create a11y.config.json in the current directory')
|
|
@@ -74,7 +74,11 @@ function buildFullReport(result, fixes) {
|
|
|
74
74
|
lines.push('');
|
|
75
75
|
}
|
|
76
76
|
if (fix) {
|
|
77
|
-
lines.push('**Why it matters:**', fix.explanation, ''
|
|
77
|
+
lines.push('**Why it matters:**', fix.explanation, '');
|
|
78
|
+
if ((!fix.fixCategory || fix.fixCategory === 'edit-element') && fix.fixedCode) {
|
|
79
|
+
lines.push('**Fixed code:**', '```html', fix.fixedCode, '```', '');
|
|
80
|
+
}
|
|
81
|
+
lines.push('**📋 Prompt for your AI assistant (Cursor / Copilot / Claude):**', '```', fix.optimalPrompt, '```', '');
|
|
78
82
|
}
|
|
79
83
|
lines.push('---', '');
|
|
80
84
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wcag-a11y",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "WCAG 2.1/2.2 accessibility auditor with AI-powered fixes. Crawls your dev server with Playwright, runs 40+ checks, and uses AI (12 providers) to generate fix prompts or patch source files directly.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wcag",
|