a11y-loop 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +409 -0
- package/THIRD-PARTY-NOTICES.md +32 -0
- package/package.json +51 -0
- package/skill/a11y-loop/SKILL.md +332 -0
- package/skill/a11y-loop/evals/evals.json +168 -0
- package/skill/a11y-loop/evals/trigger-evals.json +20 -0
- package/skill/a11y-loop/references/ai-failure-modes.md +272 -0
- package/skill/a11y-loop/references/apg-patterns.md +264 -0
- package/skill/a11y-loop/references/manual-testing.md +224 -0
- package/skill/a11y-loop/references/wcag22-quick-ref.md +224 -0
- package/src/cli.js +207 -0
- package/src/commands/audit.js +125 -0
- package/src/commands/contrast.js +141 -0
- package/src/commands/diff.js +65 -0
- package/src/lib/axe-runner.js +400 -0
- package/src/lib/browser-utils.js +221 -0
- package/src/lib/checks/dialog.js +341 -0
- package/src/lib/checks/div-button.js +87 -0
- package/src/lib/checks/focus-visible.js +296 -0
- package/src/lib/checks/keyboard.js +235 -0
- package/src/lib/checks/link-text.js +83 -0
- package/src/lib/checks/reduced-motion.js +139 -0
- package/src/lib/checks/reflow.js +101 -0
- package/src/lib/checks/target-size.js +128 -0
- package/src/lib/contrast-math.js +189 -0
- package/src/lib/diff.js +118 -0
- package/src/lib/finding.js +164 -0
- package/src/lib/fingerprint.js +0 -0
- package/src/lib/format/checklist.js +281 -0
- package/src/lib/format/human.js +175 -0
- package/src/lib/format/json.js +139 -0
- package/src/lib/format/sarif.js +111 -0
- package/src/lib/serve.js +189 -0
- package/src/lib/suggest-color.js +169 -0
- package/src/lib/wcag-map.js +271 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: a11y-loop
|
|
3
|
+
description: >-
|
|
4
|
+
Make accessible markup the default when writing UI, then verify it in a real
|
|
5
|
+
browser instead of assuming. Use whenever generating or modifying user
|
|
6
|
+
interface code (HTML, JSX/TSX, Vue, Svelte, Astro, CSS) — forms, modals,
|
|
7
|
+
menus, tabs, tables, navigation, pricing pages, dark mode, colors — and
|
|
8
|
+
whenever the user asks for an accessibility audit, an a11y check or fix, a
|
|
9
|
+
WCAG review, a contrast fix, or help with screen readers, keyboard
|
|
10
|
+
navigation, focus order, ARIA, alt text. Supplies standing generation rules
|
|
11
|
+
(semantic HTML first, ARIA discipline, APG keyboard contracts, labels, focus
|
|
12
|
+
visibility, AA contrast in light and dark, reduced motion, 24x24 targets), a
|
|
13
|
+
mandatory audit-fix-re-audit loop driven by the `a11y-loop` CLI (axe-core in
|
|
14
|
+
Chromium across default, dark, forced-colors, reduced-motion and 320px
|
|
15
|
+
passes), and honest reporting of what automation cannot judge. Keywords:
|
|
16
|
+
accessibility, a11y, WCAG 2.2 AA, ARIA, axe-core, contrast ratio, screen
|
|
17
|
+
reader. Not for backend-only work with no UI.
|
|
18
|
+
license: MIT
|
|
19
|
+
compatibility: >-
|
|
20
|
+
Verification needs Node.js >= 20 and Playwright Chromium. Install with
|
|
21
|
+
`npm i -g a11y-loop && npx playwright install chromium` (set
|
|
22
|
+
PLAYWRIGHT_BROWSERS_PATH first if you keep browsers off the system drive).
|
|
23
|
+
The generation and honesty rules apply with or without the CLI; every step
|
|
24
|
+
that says "audit" requires it.
|
|
25
|
+
metadata:
|
|
26
|
+
"a11y-loop/version": "0.1.0"
|
|
27
|
+
paths:
|
|
28
|
+
- "**/*.html"
|
|
29
|
+
- "**/*.tsx"
|
|
30
|
+
- "**/*.jsx"
|
|
31
|
+
- "**/*.vue"
|
|
32
|
+
- "**/*.svelte"
|
|
33
|
+
- "**/*.astro"
|
|
34
|
+
- "**/*.css"
|
|
35
|
+
allowed-tools: 'Bash(a11y-loop *) Bash(npx a11y-loop *) Bash(node ${CLAUDE_SKILL_DIR}/../../src/cli.js *)'
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
# a11y-loop
|
|
39
|
+
|
|
40
|
+
These are standing instructions. **For the remainder of this session, whenever
|
|
41
|
+
you write or modify UI code, apply §1 as you write it, run §2 before you call
|
|
42
|
+
the work done, and speak about the result only in the terms allowed by §3.**
|
|
43
|
+
They stay in force across turns — you do not need to be reminded.
|
|
44
|
+
|
|
45
|
+
Prompting alone does not work. UIs generated from accessibility-oriented
|
|
46
|
+
prompts measure *slightly worse* than accessibility-agnostic ones (W4A'25,
|
|
47
|
+
17.32% vs 15.93% violation rate). §1 is the setup; §2 is the product. Skipping
|
|
48
|
+
the loop reverts you to the baseline where 84% of AI-generated pages carry
|
|
49
|
+
accessibility failures.
|
|
50
|
+
|
|
51
|
+
## Resolving the CLI
|
|
52
|
+
|
|
53
|
+
Resolve once per session, first form that answers `--version` wins. Call the
|
|
54
|
+
result `A11Y` below.
|
|
55
|
+
|
|
56
|
+
1. `a11y-loop --version` — installed globally or as a project dependency.
|
|
57
|
+
2. `npx a11y-loop --version` — no install needed.
|
|
58
|
+
3. `node ${CLAUDE_SKILL_DIR}/../../src/cli.js --version` — this skill sitting
|
|
59
|
+
inside a checkout of the a11y-loop repo.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
A11Y audit <url> # dev server — primary loop path
|
|
63
|
+
A11Y audit --file path/to/page.html # served over 127.0.0.1 (never file://)
|
|
64
|
+
A11Y audit --html "<fragment>" # wrap + serve generated markup
|
|
65
|
+
--json --out report.json --sarif out.sarif --interact states.mjs
|
|
66
|
+
--headed --no-best-practice --quiet
|
|
67
|
+
A11Y contrast <fg> <bg> [--large] [--ui] [--fix] [--json]
|
|
68
|
+
A11Y diff --before before.json --after after.json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Exit codes: `0` no violations · `1` violations found · `2` tool error.
|
|
72
|
+
|
|
73
|
+
Every audit runs five passes automatically: default 1280x720, dark mode,
|
|
74
|
+
forced-colors, reduced-motion, and 320x256 (the WCAG-sanctioned equivalent of
|
|
75
|
+
400% zoom for SC 1.4.10). Dark-mode contrast failures are common and invisible
|
|
76
|
+
to a single default-mode scan, so never hand-roll a single-pass axe call
|
|
77
|
+
instead.
|
|
78
|
+
|
|
79
|
+
JSON report shape: `findings.violations`, `findings.needsReview`,
|
|
80
|
+
`findings.bestPractice`, `manualChecklist`, `summary`; each finding carries a
|
|
81
|
+
stable `fingerprint`, a `wcag` block (`sc`, `name`, `level`, `minVersion`,
|
|
82
|
+
`wcag22Only`), `act[]` rule IDs, and — for contrast — concrete
|
|
83
|
+
`suggestions[]` with hex values and before/after ratios.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## §1 Generation rules
|
|
88
|
+
|
|
89
|
+
Apply while writing, not afterwards.
|
|
90
|
+
|
|
91
|
+
**Semantic HTML first.** `<button>`, `<a href>`, `<input>`, `<select>`,
|
|
92
|
+
`<dialog>`, `<table>`, `<nav>`, `<main>`, `<h1>`–`<h6>`. Native elements bring
|
|
93
|
+
focus, keyboard behavior, and state announcement for free; a `div` brings none
|
|
94
|
+
of it. The four rules of ARIA use ([Using ARIA](https://www.w3.org/TR/using-aria/),
|
|
95
|
+
now a discontinued draft but still the canonical statement):
|
|
96
|
+
|
|
97
|
+
1. "If you can use a native HTML element or attribute with the semantics and
|
|
98
|
+
behavior you require already built in, instead of re-purposing an element
|
|
99
|
+
and adding an ARIA role, state or property to make it accessible, then do
|
|
100
|
+
so."
|
|
101
|
+
2. "Do not change native semantics, unless you really have to."
|
|
102
|
+
3. "All interactive ARIA controls must be usable with the keyboard."
|
|
103
|
+
4. "Do not use `role="presentation"` or `aria-hidden="true"` on a focusable
|
|
104
|
+
element."
|
|
105
|
+
|
|
106
|
+
**No ARIA is better than bad ARIA.** Wrong ARIA actively misrepresents the
|
|
107
|
+
interface to screen reader users — worse than none. Empirically, pages *with*
|
|
108
|
+
ARIA average more failures than pages without (WebAIM Million 2026: 59.1 vs
|
|
109
|
+
42.0). Add ARIA only when you can name the native gap it fills.
|
|
110
|
+
|
|
111
|
+
**A role is a promise.** `role="button"` commits you to Enter and Space
|
|
112
|
+
activation, focusability, and a disabled state. `role="tab"` commits you to the
|
|
113
|
+
whole arrow-key contract. ARIA creates zero behavior; an unfulfilled promise is
|
|
114
|
+
a false accessibility interface. If you cannot implement the contract, use the
|
|
115
|
+
native element or ship the plain version.
|
|
116
|
+
|
|
117
|
+
**Every APG-pattern component follows its APG keyboard contract.** Dialog,
|
|
118
|
+
tabs, accordion, menu button, combobox, disclosure, radio group, switch,
|
|
119
|
+
tooltip, listbox — contracts in
|
|
120
|
+
[references/apg-patterns.md](references/apg-patterns.md). For any widget not in
|
|
121
|
+
that file, read its APG page before implementing it.
|
|
122
|
+
|
|
123
|
+
**Names and labels.**
|
|
124
|
+
- Every input has a programmatic label: `<label for>`, wrapping `<label>`, or
|
|
125
|
+
`aria-label`. A placeholder is not a label.
|
|
126
|
+
- Every link and button has a non-empty accessible name. Icon-only controls get
|
|
127
|
+
`aria-label` (or visually-hidden text); the icon `<svg>` gets
|
|
128
|
+
`aria-hidden="true"`. Empty links (46.3% of pages) and empty buttons (30.6%)
|
|
129
|
+
are overwhelmingly icon-only controls.
|
|
130
|
+
- Visible label text must be contained in the accessible name (SC 2.5.3), so
|
|
131
|
+
voice-control users can say what they see.
|
|
132
|
+
- Group related fields in `<fieldset>` with `<legend>`.
|
|
133
|
+
|
|
134
|
+
**Structure.** `<html lang="…">` on every document. One `<h1>`; heading levels
|
|
135
|
+
descend without skipping — pick the level from the outline, style with CSS.
|
|
136
|
+
Landmarks: `<header>`, `<nav>`, `<main>` (exactly one), `<footer>`. A skip link
|
|
137
|
+
to `<main>` on full pages.
|
|
138
|
+
|
|
139
|
+
**Focus.** Always ship a visible `:focus-visible` indicator with at least 3:1
|
|
140
|
+
contrast against what surrounds it. Never `outline: none` without a
|
|
141
|
+
replacement. Never a positive `tabindex`. `tabindex="-1"` only for
|
|
142
|
+
programmatic focus targets. After a route change or view swap in an SPA, move
|
|
143
|
+
focus deliberately (to the new `<h1>` or a `tabindex="-1"` container) and
|
|
144
|
+
announce it — focus otherwise stays on a destroyed node.
|
|
145
|
+
|
|
146
|
+
**Color.** Every text/background pair must meet WCAG AA in **both** light and
|
|
147
|
+
dark mode: 4.5:1 normal text, 3:1 large text (>=24px, or >=18.5px bold), 3:1
|
|
148
|
+
for UI component boundaries and meaningful graphics. Low-contrast text is the
|
|
149
|
+
single most common failure in the wild (83.9% of pages). When choosing or
|
|
150
|
+
adjusting a color, run `A11Y contrast <fg> <bg> --fix` and take a returned
|
|
151
|
+
candidate rather than guessing. Never encode meaning in color alone — pair it
|
|
152
|
+
with text, shape, or an icon.
|
|
153
|
+
|
|
154
|
+
**Motion and pointers.** Wrap non-essential animation in
|
|
155
|
+
`@media (prefers-reduced-motion: reduce)` and actually reduce it there. Pointer
|
|
156
|
+
targets are at least 24x24 CSS px, or adequately spaced (SC 2.5.8). Anything
|
|
157
|
+
draggable needs a single-pointer alternative — a click, a button, a field (SC
|
|
158
|
+
2.5.7). No content that appears on hover unless it is dismissible, hoverable,
|
|
159
|
+
and persistent (SC 1.4.13).
|
|
160
|
+
|
|
161
|
+
**Dynamic content.** Async results, validation errors, toasts, and filter
|
|
162
|
+
counts must be announced: `role="status"` / `aria-live="polite"` (or
|
|
163
|
+
`role="alert"` for errors), with the live region present in the DOM *before*
|
|
164
|
+
the text arrives. Associate field errors via `aria-describedby` and set
|
|
165
|
+
`aria-invalid`. Reflect widget state in `aria-expanded`, `aria-checked`,
|
|
166
|
+
`aria-selected`, `aria-current` — and update it in the same handler that
|
|
167
|
+
changes the visuals.
|
|
168
|
+
|
|
169
|
+
**Before you save a UI file, re-read it against
|
|
170
|
+
[references/ai-failure-modes.md](references/ai-failure-modes.md)** — the
|
|
171
|
+
blacklist of failures generated code reproduces most: clickable `div`,
|
|
172
|
+
icon-button with no name, placeholder-as-label, skipped headings,
|
|
173
|
+
`outline: none`, `aria-labelledby` pointing at an id that does not exist,
|
|
174
|
+
`aria-hidden` on a focusable element, role without behavior, missing `lang`,
|
|
175
|
+
ignored reduced motion, SPA focus loss, unannounced updates, positive
|
|
176
|
+
`tabindex`, invented alt text.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## §2 The loop
|
|
181
|
+
|
|
182
|
+
**After any UI change, before reporting the work as done:**
|
|
183
|
+
|
|
184
|
+
1. **Make it renderable.** Start or reuse the dev server, run the build, or
|
|
185
|
+
write the fragment to a file. Pick the input mode: `<url>` for a running app
|
|
186
|
+
(preferred — it audits what actually ships), `--file` for a static page,
|
|
187
|
+
`--html` for a component fragment you just generated.
|
|
188
|
+
2. **Audit, including the states you just built.** A load-time scan finds
|
|
189
|
+
nothing inside a closed modal. Write a small interact module naming each
|
|
190
|
+
state your change introduced and pass it with `--interact`; each state is
|
|
191
|
+
driven and then audited:
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
// .a11y/states.mjs
|
|
195
|
+
export const states = {
|
|
196
|
+
'settings-dialog-open': async (page) => {
|
|
197
|
+
await page.getByRole('button', { name: 'Settings' }).click();
|
|
198
|
+
},
|
|
199
|
+
'signup-form-error': async (page) => {
|
|
200
|
+
await page.getByRole('button', { name: 'Create account' }).click();
|
|
201
|
+
},
|
|
202
|
+
'results-loaded': async (page) => {
|
|
203
|
+
await page.getByLabel('Search').fill('wcag');
|
|
204
|
+
await page.getByRole('button', { name: 'Search' }).click();
|
|
205
|
+
await page.getByRole('list', { name: 'Results' }).waitFor();
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Opened a modal? Added an error state? Added a route, a tab panel, an
|
|
211
|
+
expanded menu, an async list? Each one is a state. Audit it.
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
A11Y audit http://localhost:5173 --interact .a11y/states.mjs --json --out .a11y/run-1.json
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
3. **Read `findings.violations` and `findings.needsReview`.** Both. A
|
|
218
|
+
needsReview item is a place the engine knows it could not decide — usually
|
|
219
|
+
contrast over a gradient, image, or translucent layer. Resolve it by
|
|
220
|
+
reasoning about the source, not by ignoring it.
|
|
221
|
+
4. **Fix in the source files**, never in the report or by suppressing a rule.
|
|
222
|
+
For contrast findings, use the report's `suggestions[]` or
|
|
223
|
+
`A11Y contrast <fg> <bg> --fix`, which returns both a lighter and a darker
|
|
224
|
+
passing candidate with hue preserved; pick the one closest to the design
|
|
225
|
+
intent and apply it to the design token, not to one element.
|
|
226
|
+
5. **Re-audit to a new file, then diff.**
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
A11Y audit http://localhost:5173 --interact .a11y/states.mjs --json --out .a11y/run-2.json
|
|
230
|
+
A11Y diff --before .a11y/run-1.json --after .a11y/run-2.json
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`diff` matches by fingerprint and reports FIXED / NEW / REMAINING. NEW means
|
|
234
|
+
your fix broke something else. If the same finding alternates between FIXED
|
|
235
|
+
and NEW across iterations you are oscillating — stop, read both findings
|
|
236
|
+
together, and change the approach instead of the value.
|
|
237
|
+
6. **Repeat from 2 until `violations` is empty and `diff` reports no NEW.**
|
|
238
|
+
Then read the `manualChecklist` and carry it into your report (§3).
|
|
239
|
+
|
|
240
|
+
**Never declare UI work finished without a clean audit of the states you
|
|
241
|
+
built.** If the CLI cannot run — no Chromium, no dev server, a sandbox with no
|
|
242
|
+
browser — say so plainly, state that the change is unverified, and give the
|
|
243
|
+
exact command the user should run. Do not substitute your own reading of the
|
|
244
|
+
code for the audit.
|
|
245
|
+
|
|
246
|
+
For a fragment with no app around it, pass the markup inline with `--html`, or
|
|
247
|
+
write it to a scratch file and use `--file` (safer on any shell, and required
|
|
248
|
+
when the fragment contains quotes):
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
A11Y audit --html "<button class='icon'><svg/></button>" --json --out .a11y/frag.json
|
|
252
|
+
A11Y audit --file .a11y/scratch/price-card.html --json --out .a11y/card.json
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`--headed` when you need to watch a state fail. `--no-best-practice` when
|
|
256
|
+
best-practice noise is drowning the real violations; the WCAG-mapped findings
|
|
257
|
+
are unaffected.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## §3 Honesty rules
|
|
262
|
+
|
|
263
|
+
The claims below are the ones that produced a $1,000,000 FTC penalty against an
|
|
264
|
+
accessibility overlay vendor in 2025 and that 800+ signatories of the
|
|
265
|
+
[Overlay Fact Sheet](https://overlayfactsheet.com/) exist to refute. They are
|
|
266
|
+
also simply not what an automated pass shows.
|
|
267
|
+
|
|
268
|
+
**Never say, about anything this loop produced:** "accessible", "fully
|
|
269
|
+
accessible", "WCAG compliant", "meets WCAG 2.2 AA", "passes WCAG",
|
|
270
|
+
"guarantees", "ensures", "certified", "100% accessible", "makes your app
|
|
271
|
+
accessible", "no manual testing needed", "reduces legal risk", "ADA
|
|
272
|
+
compliant". Never present a score or grade as a verdict.
|
|
273
|
+
|
|
274
|
+
**Say instead:** "`a11y-loop audit` found no automatically detectable failures
|
|
275
|
+
in the states I drove (default, dark, forced-colors, reduced-motion, 320px
|
|
276
|
+
reflow; states: settings-dialog-open, signup-form-error). Here is what still
|
|
277
|
+
needs human review."
|
|
278
|
+
|
|
279
|
+
**Always report the coverage limit with its denominator.** Automated testing
|
|
280
|
+
reaches a minority of WCAG failures, and the honest figures disagree because
|
|
281
|
+
they measure different things: Deque puts automation at **57% of issues by
|
|
282
|
+
volume of individual instances** across 13,000+ pages; Adrian Roselli counts
|
|
283
|
+
**17 of the 55** WCAG 2.2 A/AA criteria as having any ACT-approved automated
|
|
284
|
+
rule (31%), and not fully covered even there; accessible.org counts **7 of 55**
|
|
285
|
+
as reliably flagged (13%). Nine A/AA criteria cannot be meaningfully tested by
|
|
286
|
+
any tool. Cite one figure with its denominator, not a bare percentage.
|
|
287
|
+
|
|
288
|
+
**Always surface the `manualChecklist`.** It is a deliverable, not a
|
|
289
|
+
disclaimer — it names the criteria automation could not judge for the
|
|
290
|
+
components you actually built. Include it in your summary to the user, not just
|
|
291
|
+
in the JSON.
|
|
292
|
+
|
|
293
|
+
**`needsReview` findings are unresolved uncertainty, never passes.** Report
|
|
294
|
+
them as open questions with what you did about each.
|
|
295
|
+
|
|
296
|
+
**Mark every alt text and accessible name you wrote as DRAFT needing human
|
|
297
|
+
confirmation.** You cannot see the image, and hallucinated image descriptions
|
|
298
|
+
are a documented failure mode of generated code. `alt="decorative image"`
|
|
299
|
+
passes every automated check and is worse than `alt=""`. Write the draft, label
|
|
300
|
+
it, and ask for confirmation:
|
|
301
|
+
|
|
302
|
+
> `alt="Two people reviewing a document at a desk"` — **DRAFT**, written from
|
|
303
|
+
> filename and surrounding copy; please confirm it describes the actual image,
|
|
304
|
+
> or set `alt=""` if it is decorative.
|
|
305
|
+
|
|
306
|
+
**Recommend real assistive-technology testing before production**, and say
|
|
307
|
+
which is cheapest to start with: NVDA (free, Windows), VoiceOver (built into
|
|
308
|
+
macOS/iOS). Automated checks and your own reading cannot tell you whether the
|
|
309
|
+
interface is usable — only people using it can, and testing with disabled
|
|
310
|
+
users is what actually closes that gap. See
|
|
311
|
+
[references/manual-testing.md](references/manual-testing.md) for the full
|
|
312
|
+
not-automatable list and a keyboard script a non-expert can run in five
|
|
313
|
+
minutes.
|
|
314
|
+
|
|
315
|
+
**Attribute the engine.** Findings come from axe-core (MPL-2.0, Deque Systems)
|
|
316
|
+
and the WCAG success criteria are W3C's. Cite findings as
|
|
317
|
+
`SC 4.1.2 Name, Role, Value (Level A) · ACT 97a4e1 · axe: button-name` — the
|
|
318
|
+
success criterion is the obligation; ACT rule IDs are informative secondary
|
|
319
|
+
identifiers, not the source of the requirement.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## References
|
|
324
|
+
|
|
325
|
+
Read on demand; none of it costs context until you open it.
|
|
326
|
+
|
|
327
|
+
| File | Use it when |
|
|
328
|
+
|---|---|
|
|
329
|
+
| [references/wcag22-quick-ref.md](references/wcag22-quick-ref.md) | You need the exact SC number, level, contrast threshold, or what is new in WCAG 2.2 |
|
|
330
|
+
| [references/apg-patterns.md](references/apg-patterns.md) | You are building a dialog, tabs, accordion, menu, combobox, disclosure, radio group, switch, tooltip, or listbox |
|
|
331
|
+
| [references/ai-failure-modes.md](references/ai-failure-modes.md) | Before saving generated UI code, and when a finding needs a wrong-vs-right example |
|
|
332
|
+
| [references/manual-testing.md](references/manual-testing.md) | You are writing the §3 hand-off, or a user asks what the audit did not check |
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "a11y-loop",
|
|
3
|
+
"schema_note": "skill-creator evals.json schema (skill_name + evals[] with id/prompt/expected_output/expectations). The non-standard `should_trigger` field is additive metadata for description tuning; the machine-readable trigger set the skill-creator run_eval.py script consumes is a bare array in trigger-evals.json alongside this file.",
|
|
4
|
+
"evals": [
|
|
5
|
+
{
|
|
6
|
+
"id": 1,
|
|
7
|
+
"should_trigger": true,
|
|
8
|
+
"prompt": "Create a signup page at src/signup.html with fields for full name, email, password and a country dropdown, plus a submit button. Style it with a plain stylesheet, no framework.",
|
|
9
|
+
"expected_output": "An accessible signup page whose generated markup was audited in a real browser and iterated to zero violations, reported without any conformance claim.",
|
|
10
|
+
"expectations": [
|
|
11
|
+
"Every input has an associated <label for> (or a wrapping <label>); no field relies on a placeholder as its label",
|
|
12
|
+
"Inputs carry appropriate autocomplete tokens such as name, email, current-password or new-password",
|
|
13
|
+
"The document has <html lang> set and a single <h1>",
|
|
14
|
+
"A visible :focus-visible indicator is defined in CSS and no rule sets outline: none without a replacement",
|
|
15
|
+
"An a11y-loop audit command was actually run against the generated page (--file or --url, not file://)",
|
|
16
|
+
"Any violations reported by the audit were fixed in the source file and the page was re-audited",
|
|
17
|
+
"The final summary does not claim the page is accessible, compliant, or that it passes WCAG",
|
|
18
|
+
"The final summary surfaces the manual checklist or otherwise names what still needs human review"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": 2,
|
|
23
|
+
"should_trigger": true,
|
|
24
|
+
"prompt": "Add a Settings dialog to the React app in src/App.jsx. It should open from a gear icon button in the header and contain two toggles and a Save button.",
|
|
25
|
+
"expected_output": "A dialog implementing the APG modal-dialog contract, with the opened state driven and audited via --interact, not just the closed page.",
|
|
26
|
+
"expectations": [
|
|
27
|
+
"The gear icon button has a non-empty accessible name (aria-label or visually hidden text) and the icon itself is aria-hidden",
|
|
28
|
+
"The dialog has an accessible name via aria-labelledby or aria-label, focus moves into it on open, Escape closes it, and focus returns to the trigger on close",
|
|
29
|
+
"Focus is constrained to the dialog while it is open (native <dialog> showModal, inert, or an explicit trap)",
|
|
30
|
+
"The toggles use role=\"switch\" with aria-checked, or a native checkbox, and the state attribute is updated in the same handler as the visual change",
|
|
31
|
+
"An --interact module was written that opens the dialog, and the audit was run with it so the open state was audited",
|
|
32
|
+
"The response does not claim the dialog is accessible or compliant"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": 3,
|
|
37
|
+
"should_trigger": true,
|
|
38
|
+
"prompt": "Build a three-tier pricing section for our marketing site: Starter, Team, Enterprise. Cards with a feature list each and a Choose plan button. Make it look modern.",
|
|
39
|
+
"expected_output": "A pricing section with a correct heading outline, named buttons, AA contrast in light and dark mode, and a clean audit including the 320px reflow pass.",
|
|
40
|
+
"expectations": [
|
|
41
|
+
"Heading levels descend without skipping (the plan names are not styled-down h4s under an h1) and visual size is set in CSS rather than by heading level",
|
|
42
|
+
"Each 'Choose plan' button has an accessible name that distinguishes which plan it selects",
|
|
43
|
+
"Feature lists use real list markup",
|
|
44
|
+
"Text and background color pairs were checked against WCAG AA, using a11y-loop contrast or the audit's contrast findings rather than being assumed",
|
|
45
|
+
"An a11y-loop audit was run and the 320x256 reflow pass produced no unresolved horizontal overflow",
|
|
46
|
+
"The final summary avoids the words compliant, guarantees, and fully accessible"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": 4,
|
|
51
|
+
"should_trigger": true,
|
|
52
|
+
"prompt": "Our brand color is #7C9EC4 and we use it for body text links on white, and for button labels on a #F3F4F6 grey. Add a dark theme too. Update src/tokens.css.",
|
|
53
|
+
"expected_output": "Contrast-checked token values for light and dark themes, with concrete failing/passing ratios reported and candidates chosen from the CLI rather than guessed.",
|
|
54
|
+
"expectations": [
|
|
55
|
+
"a11y-loop contrast was run for the stated color pairs, including the --fix flag or the report suggestions, rather than the ratios being estimated",
|
|
56
|
+
"The reported contrast ratio for #7C9EC4 on white is identified as failing the 4.5:1 requirement for normal text",
|
|
57
|
+
"Replacement colors are applied to the design tokens rather than to individual elements",
|
|
58
|
+
"The dark theme values were contrast-checked separately, not assumed to inherit from the light theme",
|
|
59
|
+
"Link styling does not rely on color alone to distinguish links in body text",
|
|
60
|
+
"The response cites SC 1.4.3 (and 1.4.11 where UI boundaries are involved) with the ratio it measured"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": 5,
|
|
65
|
+
"should_trigger": true,
|
|
66
|
+
"prompt": "The dashboard needs tabs: Overview, Billing, Members. Add them to src/components/Dashboard.tsx.",
|
|
67
|
+
"expected_output": "A tabs implementation following the APG contract, with each tab panel state audited.",
|
|
68
|
+
"expectations": [
|
|
69
|
+
"The implementation uses role=\"tablist\" / role=\"tab\" / role=\"tabpanel\" with aria-selected, aria-controls and aria-labelledby wired to ids that resolve",
|
|
70
|
+
"Roving tabindex is implemented: the selected tab has tabindex 0 and the others -1",
|
|
71
|
+
"Arrow key navigation between tabs plus Home and End are implemented, matching the APG keyboard contract",
|
|
72
|
+
"The apg-patterns.md reference was consulted or its contract is visibly followed rather than approximated",
|
|
73
|
+
"An audit was run covering more than the initially selected panel, for example via an --interact module that selects each tab",
|
|
74
|
+
"No conformance claim appears in the summary"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": 6,
|
|
79
|
+
"should_trigger": true,
|
|
80
|
+
"prompt": "Run an accessibility audit on http://localhost:3000 and tell me what's wrong.",
|
|
81
|
+
"expected_output": "An audit run through the CLI with violations and needsReview both reported, coverage stated with a denominator, and the manual checklist surfaced.",
|
|
82
|
+
"expectations": [
|
|
83
|
+
"a11y-loop audit was run against the URL, producing JSON output",
|
|
84
|
+
"Both findings.violations and findings.needsReview are reported, with needsReview presented as unresolved uncertainty rather than as passes",
|
|
85
|
+
"Findings are cited by WCAG success criterion number, name and level",
|
|
86
|
+
"The coverage limitation is stated with its denominator (for example 57% of issue instances by volume, or 17 of 55 criteria having any automated rule)",
|
|
87
|
+
"The manual checklist is included in the response to the user, not only left in the JSON file",
|
|
88
|
+
"Real assistive technology testing is recommended before production",
|
|
89
|
+
"The response does not present a score or grade as a verdict"
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": 7,
|
|
94
|
+
"should_trigger": true,
|
|
95
|
+
"prompt": "A reviewer says the icon buttons in src/Toolbar.jsx aren't accessible. Can you check and fix them?",
|
|
96
|
+
"expected_output": "Accessible names added to icon-only controls, decorative icons hidden, target size checked, and the fix verified by audit rather than asserted.",
|
|
97
|
+
"expectations": [
|
|
98
|
+
"Each icon-only button receives an accessible name via aria-label or visually hidden text",
|
|
99
|
+
"The decorative svg or icon element is marked aria-hidden=\"true\"",
|
|
100
|
+
"Target size is considered against the 24x24 CSS px minimum of SC 2.5.8",
|
|
101
|
+
"The change was verified with an a11y-loop audit rather than only by inspection",
|
|
102
|
+
"Any accessible name inferred from an icon shape rather than existing text is flagged as needing human confirmation",
|
|
103
|
+
"SC 4.1.2 Name, Role, Value is cited"
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"id": 8,
|
|
108
|
+
"should_trigger": true,
|
|
109
|
+
"prompt": "Add alt text to all the images in src/pages/about.html.",
|
|
110
|
+
"expected_output": "Draft alt text clearly labelled as unverified, with decorative images given empty alt, and an explicit request for human confirmation.",
|
|
111
|
+
"expectations": [
|
|
112
|
+
"Every alt value the model wrote is explicitly labelled DRAFT or otherwise marked as needing human confirmation",
|
|
113
|
+
"The response states that the images were not seen and the descriptions are inferred from filename, context or surrounding copy",
|
|
114
|
+
"Images judged decorative are given alt=\"\" rather than a description or a phrase like 'decorative image'",
|
|
115
|
+
"The user is asked to confirm or replace each draft",
|
|
116
|
+
"The response does not claim the images are now accessible"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"id": 9,
|
|
121
|
+
"should_trigger": true,
|
|
122
|
+
"prompt": "I fixed the contrast issues you found. Did it work, and did I break anything?",
|
|
123
|
+
"expected_output": "A re-audit plus a fingerprint diff against the previous report, reported as FIXED / NEW / REMAINING.",
|
|
124
|
+
"expectations": [
|
|
125
|
+
"A fresh audit was run to a new report file rather than reusing the earlier one",
|
|
126
|
+
"a11y-loop diff was run with --before and --after pointing at the two reports",
|
|
127
|
+
"The answer distinguishes FIXED, NEW and REMAINING findings",
|
|
128
|
+
"Any NEW findings are treated as regressions introduced by the fix and addressed rather than reported and left",
|
|
129
|
+
"The loop is only declared finished when violations is empty and diff reports no NEW"
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": 10,
|
|
134
|
+
"should_trigger": false,
|
|
135
|
+
"prompt": "Refactor the argument parsing in bin/deploy.js to use node:util parseArgs instead of the hand-rolled loop, and keep the existing flags working.",
|
|
136
|
+
"expected_output": "A straightforward CLI refactor with no accessibility work and no audit attempted.",
|
|
137
|
+
"expectations": [
|
|
138
|
+
"The refactor is completed using node:util parseArgs with the existing flags preserved",
|
|
139
|
+
"No a11y-loop command was run",
|
|
140
|
+
"No accessibility manual checklist, WCAG citation, or contrast discussion appears in the response",
|
|
141
|
+
"No unrequested ARIA, alt text, or label changes were introduced"
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"id": 11,
|
|
146
|
+
"should_trigger": false,
|
|
147
|
+
"prompt": "This query times out on the reports table. Can you rewrite it to use a window function instead of the correlated subquery? SELECT id, (SELECT COUNT(*) FROM events e WHERE e.report_id = r.id) AS n FROM reports r;",
|
|
148
|
+
"expected_output": "A rewritten SQL query with no accessibility content.",
|
|
149
|
+
"expectations": [
|
|
150
|
+
"A window-function rewrite of the query is provided",
|
|
151
|
+
"No a11y-loop command was run",
|
|
152
|
+
"No WCAG success criteria, contrast ratios, or accessibility checklist appear in the response"
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"id": 12,
|
|
157
|
+
"should_trigger": false,
|
|
158
|
+
"prompt": "There's a typo in README.md — 'recieve' should be 'receive'. Fix it.",
|
|
159
|
+
"expected_output": "A one-word documentation fix, nothing else.",
|
|
160
|
+
"expectations": [
|
|
161
|
+
"The typo is corrected in README.md",
|
|
162
|
+
"No a11y-loop command was run",
|
|
163
|
+
"No accessibility guidance, audit, or checklist appears in the response",
|
|
164
|
+
"No other files were modified"
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "query": "Create a signup page at src/signup.html with name, email, password and a country dropdown", "should_trigger": true },
|
|
3
|
+
{ "query": "Add a Settings dialog to src/App.jsx that opens from a gear icon button in the header", "should_trigger": true },
|
|
4
|
+
{ "query": "Build a three-tier pricing section with cards and a Choose plan button, make it look modern", "should_trigger": true },
|
|
5
|
+
{ "query": "Our brand color is #7C9EC4 on white for body links — add a dark theme to src/tokens.css", "should_trigger": true },
|
|
6
|
+
{ "query": "The dashboard needs tabs: Overview, Billing, Members. Add them to Dashboard.tsx", "should_trigger": true },
|
|
7
|
+
{ "query": "Style the nav bar so it collapses into a hamburger menu on mobile", "should_trigger": true },
|
|
8
|
+
{ "query": "Run an accessibility audit on http://localhost:3000 and tell me what's wrong", "should_trigger": true },
|
|
9
|
+
{ "query": "A reviewer says the icon buttons in src/Toolbar.jsx aren't accessible, can you check and fix them", "should_trigger": true },
|
|
10
|
+
{ "query": "Add alt text to all the images in src/pages/about.html", "should_trigger": true },
|
|
11
|
+
{ "query": "Is this modal usable with a screen reader and the keyboard alone?", "should_trigger": true },
|
|
12
|
+
{ "query": "Check whether our color palette meets WCAG AA contrast", "should_trigger": true },
|
|
13
|
+
{ "query": "I fixed the contrast issues you found — did it work, and did I break anything?", "should_trigger": true },
|
|
14
|
+
{ "query": "Refactor the argument parsing in bin/deploy.js to use node:util parseArgs", "should_trigger": false },
|
|
15
|
+
{ "query": "This query times out, rewrite the correlated subquery as a window function", "should_trigger": false },
|
|
16
|
+
{ "query": "There's a typo in README.md, 'recieve' should be 'receive'", "should_trigger": false },
|
|
17
|
+
{ "query": "Add retry with exponential backoff to the S3 upload in workers/ingest.js", "should_trigger": false },
|
|
18
|
+
{ "query": "Write a GitHub Actions workflow that runs the unit tests on push", "should_trigger": false },
|
|
19
|
+
{ "query": "Why is my Express middleware running twice per request?", "should_trigger": false }
|
|
20
|
+
]
|