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 CHANGED
@@ -2,17 +2,17 @@
2
2
 
3
3
  [![CI](https://github.com/Dannyplusplus12/WCAG-A11y/actions/workflows/ci.yml/badge.svg)](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` goes further. It 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**.
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 + generate AI prompts you paste into Cursor, Copilot, or Claude
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, no setup:
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
- Scanning http://localhost:3000...
29
-
30
- /
31
- ✖ critical img-alt 3 violations
32
- ✖ serious color-contrast-text 2 violations
33
- ✖ serious label-missing 1 violation
34
- ✖ moderate no-positive-tabindex 1 violation
35
-
36
- 7 violations across 1 page
37
-
38
- Generating AI fixes for 7 violations...
39
-
40
- ────────────────────────────────────────────
41
- [img-alt] 3 elements affected
42
- #hero-img #logo #banner
43
-
44
- Why it matters:
45
- Screen readers cannot describe the image to blind users without an alt attribute.
46
- Users relying on assistive technology receive no information about the image content.
47
-
48
- Fixed HTML:
49
- <img src="banner.jpg" alt="Summer sale — up to 50% off">
50
-
51
- Prompt for your AI editor:
52
- Fix accessibility: 3 <img> elements (#hero-img, #logo, #banner) are missing alt
53
- attributes, violating WCAG 1.1.1. Add descriptive alt text to each image.
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
- The **prompt** at the end of each fix is what you copy into Cursor, Copilot, or Claude. It includes the affected selectors, the WCAG rule, and exactly what needs to change no rewriting needed.
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
- ## Setup
78
+ ## Quick start
70
79
 
71
80
  ```bash
72
- wcag-a11y init # Gemini (free, default)
73
- wcag-a11y init --provider openai # OpenAI
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
- ### `wcag-a11y demo`
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
- | Flag | Description |
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
- ### `wcag-a11y init`
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 <pages...>` | `/` | One or more paths to scan. Separate with spaces: `--pages / /about /contact` |
141
- | `-c, --crawl` | off | Follow same-origin links and scan all reachable pages automatically |
142
- | `--no-report` | on | Skip saving scan output to `a11y-report.md` (report is saved by default) |
143
- | `--no-ai` | on | Skip AI fix generation — scan runs faster and prints violations only |
144
- | `--no-explain` | on | Print only the ready-to-paste prompt for each fix, without the AI explanation |
145
- | `--terminal` | off | Print violations summary and AI fix prompts to terminal |
146
- | `--fast-mode` | off | Output only AI fix prompts — no summaries, explanations, or progress messages |
147
- | `--group <strategy>` | `rule` | `rule` (default) groups all violations of the same type into one fix prompt. `none` produces a separate prompt per element. Use `none` when violations of the same rule need different fixes |
148
- | `--ci` | off | Exit with code `1` if any violations are found. Use this to fail a CI pipeline |
149
- | `--provider <name>` | from config | Override the AI provider for this run. See [AI Providers](#ai-providers) for valid names. Does not modify the config file |
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
- # Dry-run: scan and show what would change (safe, no files written)
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 violation(s)
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 of your running dev server. Required unless `--from-report` is used |
202
- | `-p, --pages <pages...>` | `/` | Specific pages to scan |
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 scanning. Useful when you already ran `scan --report` and just want to apply fixes |
205
- | `--apply` | off | Write patched files to disk (dry-run without this flag) |
206
- | `--provider <name>` | from config | Override AI provider for this run. See [AI Providers](#ai-providers) for valid names |
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
- > **Tip:** Always run without `--apply` first to review the diff. The dry-run is safe — nothing is written to disk.
165
+ ### `wcag-a11y init`
209
166
 
210
- **Common workflow:** run `scan --report` to generate a report for review, then run `fix --from-report --apply` to patch the files — no second browser crawl needed.
167
+ Create `a11y.config.json` pre-configured for your chosen provider.
211
168
 
212
169
  ```bash
213
- wcag-a11y scan -u http://localhost:3000 # generates a11y-report.md automatically
214
- wcag-a11y fix --from-report --apply # patch files from that report
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 are supported. Set your provider in `a11y.config.json` or override per-run with `--provider <name>`.
192
+ 12 providers supported. Configure once in `a11y.config.json`, or override per-run with `--provider`.
222
193
 
223
- | Provider | `--provider` name | Default model | API key source |
194
+ | Provider | `--provider` | Default model | Notes |
224
195
  |---|---|---|---|
225
- | Google Gemini | `gemini` *(default)* | `gemini-2.5-flash` | [aistudio.google.com](https://aistudio.google.com) free tier |
226
- | OpenAI | `openai` | `gpt-4o-mini` | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
227
- | Anthropic | `anthropic` | `claude-sonnet-4-6` | [console.anthropic.com](https://console.anthropic.com) |
228
- | Mistral | `mistral` | `mistral-large-latest` | [console.mistral.ai](https://console.mistral.ai) |
229
- | Groq | `groq` | `llama-3.3-70b-versatile` | [console.groq.com](https://console.groq.com) |
230
- | Cohere | `cohere` | `command-r-plus` | [dashboard.cohere.com](https://dashboard.cohere.com) |
231
- | xAI | `xai` | `grok-2` | [console.x.ai](https://console.x.ai) |
232
- | DeepSeek | `deepseek` | `deepseek-chat` | [platform.deepseek.com](https://platform.deepseek.com) |
233
- | Together AI | `together` | `meta-llama/Llama-3-70b-chat-hf` | [api.together.xyz](https://api.together.xyz) |
234
- | Perplexity | `perplexity` | `llama-3.1-sonar-large-128k-online` | [perplexity.ai/settings/api](https://www.perplexity.ai/settings/api) |
235
- | Azure OpenAI | `azure-openai` | *(your deployment)* | [portal.azure.com](https://portal.azure.com) |
236
- | Ollama | `ollama` | `llama3` | Nonerun `ollama serve` locally |
237
-
238
- All models are configurable. Change the model field in `a11y.config.json` to use any model your API key has access to. If the AI response is unparseable, the tool generates a fix prompt directly from the violation data so you always get something actionable.
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` | Localno 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 (`a11y.config.json`)
213
+ ## Config
243
214
 
244
- Only the fields for your active `provider` are required. This file is gitignored by default.
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 categories, mapped to WCAG 2.1/2.2 success criteria.
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) => `- ${s}`).join('\n');
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\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
28
- : `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
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: v.html,
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) => `${i + 1}. Rule: ${g.ruleId} | WCAG ${g.wcag} (Level ${g.level}) | Impact: ${g.impact}
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 corrected snippet in ${syntax} syntax (component/template code only no imports, no surrounding boilerplate)`
27
- : `"fixedCode": the corrected HTML snippet only (no explanation, just code)`;
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) list the affected selectors and HTML snippets, (3) state the exact change needed in ${syntax} syntax. Focus solely on the fix.`
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) list the affected selectors and HTML snippets, (3) state the exact change needed. Focus solely on the fix.`;
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.0');
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, '', '**Fixed code:**', '```html', fix.fixedCode, '```', '', '**📋 Prompt for your AI assistant (Cursor / Copilot / Claude):**', '```', fix.optimalPrompt, '```', '');
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.0",
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",