Accessibility Auditor
Systematic WCAG 2.2 AA audit protocol organized by the four accessibility principles: Perceivable, Operable, Understandable, and Robust (POUR).
When to Apply
Use this skill when:
- Auditing a page, screen, or component for WCAG 2.2 AA compliance
- Reviewing a PR for accessibility regressions
- Building new interactive components that need accessibility
- Preparing for an accessibility compliance review
- Fixing reported accessibility issues
Audit Protocol
Principle 1: Perceivable
Information and UI components must be presentable in ways users can perceive.
| Criterion |
What to Check |
Automated? |
| 1.1.1 Non-text Content |
All <img> have alt; decorative images have alt="" |
Yes |
| 1.3.1 Info and Relationships |
Semantic HTML, heading hierarchy, form labels |
Partial |
| 1.3.2 Meaningful Sequence |
DOM order matches visual order |
Manual |
| 1.3.3 Sensory Characteristics |
Instructions don't rely solely on shape, size, or position |
Manual |
| 1.4.1 Use of Color |
Information not conveyed by color alone |
Manual |
| 1.4.3 Contrast (Minimum) |
4.5:1 normal text, 3:1 large text |
Yes |
| 1.4.4 Resize Text |
Text resizable to 200% without loss |
Manual |
| 1.4.5 Images of Text |
No text rendered as images (except logos) |
Yes |
| 1.4.11 Non-text Contrast |
3:1 for UI components and graphical objects |
Partial |
Principle 2: Operable
UI components and navigation must be operable.
| Criterion |
What to Check |
Automated? |
| 2.1.1 Keyboard |
All functionality available via keyboard |
Manual |
| 2.1.2 No Keyboard Trap |
Focus can be moved away from any component |
Manual |
| 2.4.1 Bypass Blocks |
Skip-to-content link present |
Yes |
| 2.4.2 Page Titled |
Descriptive <title> on every page |
Yes |
| 2.4.3 Focus Order |
Logical tab order |
Manual |
| 2.4.4 Link Purpose |
Link text describes destination (no "click here") |
Partial |
| 2.4.6 Headings and Labels |
Descriptive headings, no skipped levels |
Yes |
| 2.4.7 Focus Visible |
Visible focus indicator on all focusable elements |
Partial |
| 2.4.11 Focus Not Obscured (2.2) |
Focused element not fully hidden by other content |
Manual |
| 2.4.13 Focus Appearance (2.2, AAA — house rule, not required for AA) |
Focus indicator has 3:1 contrast, 2px minimum |
Partial |
| 2.5.7 Dragging Movements (2.2) |
Drag operations have single-pointer alternative |
Manual |
| 2.5.8 Target Size (2.2) |
Interactive targets minimum 24x24px (44x44px mobile) |
Yes |
Principle 3: Understandable
Information and UI operation must be understandable.
| Criterion |
What to Check |
Automated? |
| 3.1.1 Language of Page |
lang attribute on <html> element |
Yes |
| 3.1.2 Language of Parts |
lang attribute on foreign language content |
Manual |
| 3.2.1 On Focus |
No context change on focus alone |
Manual |
| 3.2.2 On Input |
No unexpected context change on input |
Manual |
| 3.3.1 Error Identification |
Errors described in text, not just color |
Manual |
| 3.3.2 Labels or Instructions |
Form inputs have labels; complex inputs have instructions |
Yes |
| 3.2.6 Consistent Help (2.2) |
Help mechanisms in same relative location across pages |
Manual |
| 3.3.7 Redundant Entry (2.2) |
Previously entered info auto-populated or selectable |
Manual |
| 3.3.8 Accessible Authentication (2.2) |
No cognitive function test for auth (allow paste, managers) |
Manual |
Principle 4: Robust
Content must be robust enough for assistive technologies.
| Criterion |
What to Check |
Automated? |
| 4.1.2 Name, Role, Value |
Custom components have proper ARIA roles/states |
Partial |
| 4.1.3 Status Messages |
Dynamic content uses aria-live regions |
Partial |
Automated Checks
Run these checks programmatically:
Code-Level (Grep/Read)
Check Pattern to Search
───────────────────────────── ──────────────────────────────
Missing alt text <img without alt=
Div as button <div.*onClick (without role="button")
Missing form labels <input without associated <label>
Empty links <a>...</a> with no text content
Missing lang attribute <html without lang=
Heading hierarchy h1, h2, h3 order (no skipped levels)
Focus outline removed outline: none without replacement
Hardcoded color as info Color-only status indicators
Token contrast is computable — do not send it to a browser
Audit the tokens at their source, not only the rendered page. A design token pair is two static
HSL values in a file; the WCAG ratio between them is arithmetic, so it belongs in a test, not in a
manual DevTools pass. Nobody opens DevTools 36 times, which is exactly how a contrast failure
survives: not because it is subtle, but because checking it by hand does not scale and so does not
happen.
Every --x / --x-foreground pair in globals.css (or wherever the tokens live) is a case:
// The formula and a ready-made contrastRatio() live in
// @skills/std-design-system/references/defining-tokens.md — import, do not re-derive.
import { contrastRatio } from './check-contrast';
import { light, dark } from './tokens';
// Iterate the PAIRS — do not hand-write two cases and call it covered.
const PAIRS = ['primary', 'success', 'error', 'warning', 'info'] as const;
describe.each([['light', light], ['dark', dark]])('%s token contrast', (_name, theme) => {
it.each(PAIRS)('%s-foreground on %s clears 4.5:1', (token) => {
expect(contrastRatio(theme[`${token}Foreground`], theme[token])).toBeGreaterThanOrEqual(4.5);
});
it('muted-foreground on muted clears 4.5:1', () => {
expect(contrastRatio(theme.mutedForeground, theme.muted)).toBeGreaterThanOrEqual(4.5);
});
});
Two traps this catches that a page-level audit does not:
- A
-foreground token is verified against its SOLID surface. bg-success/10 text-success-foreground is a 10% tint under near-white text — around 1:1, invisible — and no
static check of the token pair sees it, because the pair itself is fine. On a tint, use
text-foreground.
- Presets and themes are copies. If the project ships more than one theme, every theme is a
case. A fix applied to the spec does not reach the preset beside it.
Tool-Based
| Tool |
Purpose |
Integration |
axe-core / jest-axe |
Automated WCAG checks in Vitest |
npm run test |
| Token contrast test (above) |
Every token pair, every theme |
npm run test — not manual |
| Chrome DevTools |
Accessibility tree, live contrast spot-checks |
Manual |
| VoiceOver (macOS) |
Screen reader testing |
Manual |
| NVDA (Windows) |
Screen reader testing |
Manual |
| Lighthouse |
Accessibility score |
CI pipeline |
Output Format
Present findings as:
[SEVERITY] | File:Line | WCAG Criterion | Finding | Remediation
───────────┼──────────┼────────────────┼─────────┼────────────
CRITICAL | path:42 | 2.4.7 | No focus indicator on submit button | Add focus-visible:ring-2 focus-visible:ring-ring
MAJOR | path:15 | 1.1.1 | <img> missing alt text | Add descriptive alt or alt="" for decorative
MINOR | path:88 | 2.4.4 | Link text is "click here" | Use descriptive text: "View order details"
Severity Levels:
- CRITICAL: Blocks user access entirely (no keyboard access, missing essential alt text)
- MAJOR: Significantly impairs experience (low contrast, missing labels)
- MINOR: Degrades experience but workaround exists (non-descriptive link text)
- INFO: Best practice recommendation (could improve but compliant)
Summary Metrics
End each audit with:
| Metric |
Value |
| Total checks performed |
X |
| CRITICAL issues |
X |
| MAJOR issues |
X |
| MINOR issues |
X |
| Estimated compliance |
X% (passing checks / total checks) |
| WCAG 2.2 AA conformance |
Yes / No (any CRITICAL = No) |
Full References
references/wcag-22-checklist.md — Complete WCAG 2.2 AA checklist by principle
references/aria-patterns.md — ARIA widget patterns with keyboard specifications
Owned elsewhere
std-accessibility is the WCAG rule set — it is scoped to .tsx/.jsx under
src/, app/, and components/. Note the edge it does not cover: its paths: are component
files, so it does not load on globals.css or a token file. Contrast decisions get made
where the tokens are defined, and that is std-design-system's territory (it is scoped to
**/globals.css, **/styles/**, **/tailwind.config.*). Auditing a colour system means opening
both.
@skills/std-design-system/references/defining-tokens.md — the contrastRatio()
implementation, the Vitest wiring, and the rule for which side to fix: darken the foreground
for a brand token (the surface is the brand decision), darken the surface for a semantic
status token (which green is not the convention; "green means success" is).
@skills/theming/references/design-tokens.md — the canonical token values and their measured
ratios. @skills/theming/references/theme-presets.md — three presets meant to be copied
wholesale, which is why each is audited as its own theme.
accessibility-checker.py warns on .tsx/.jsx/.css/.scss (semantic HTML, alt text, label
association, focus indicators, ARIA misuse). It does not compute contrast — that is the test
above, because the ratio needs the token values, not the markup.
1---2name: accessibility-auditor3description: |4 WCAG 2.2 AA accessibility audit with 4-principle framework, automated checks,5 color contrast analysis, keyboard navigation testing, and ARIA pattern validation.6 Triggers on "accessibility audit", "WCAG audit", "a11y audit", "accessibility check",7 "screen reader test", "keyboard accessibility", or "accessibility compliance".8model: sonnet9---1011# Accessibility Auditor1213Systematic WCAG 2.2 AA audit protocol organized by the four accessibility principles: Perceivable, Operable, Understandable, and Robust (POUR).1415## When to Apply1617Use this skill when:18- Auditing a page, screen, or component for WCAG 2.2 AA compliance19- Reviewing a PR for accessibility regressions20- Building new interactive components that need accessibility21- Preparing for an accessibility compliance review22- Fixing reported accessibility issues2324## Audit Protocol2526### Principle 1: Perceivable2728Information and UI components must be presentable in ways users can perceive.2930| Criterion | What to Check | Automated? |31|-----------|---------------|-----------|32| 1.1.1 Non-text Content | All `<img>` have `alt`; decorative images have `alt=""` | Yes |33| 1.3.1 Info and Relationships | Semantic HTML, heading hierarchy, form labels | Partial |34| 1.3.2 Meaningful Sequence | DOM order matches visual order | Manual |35| 1.3.3 Sensory Characteristics | Instructions don't rely solely on shape, size, or position | Manual |36| 1.4.1 Use of Color | Information not conveyed by color alone | Manual |37| 1.4.3 Contrast (Minimum) | 4.5:1 normal text, 3:1 large text | Yes |38| 1.4.4 Resize Text | Text resizable to 200% without loss | Manual |39| 1.4.5 Images of Text | No text rendered as images (except logos) | Yes |40| 1.4.11 Non-text Contrast | 3:1 for UI components and graphical objects | Partial |4142### Principle 2: Operable4344UI components and navigation must be operable.4546| Criterion | What to Check | Automated? |47|-----------|---------------|-----------|48| 2.1.1 Keyboard | All functionality available via keyboard | Manual |49| 2.1.2 No Keyboard Trap | Focus can be moved away from any component | Manual |50| 2.4.1 Bypass Blocks | Skip-to-content link present | Yes |51| 2.4.2 Page Titled | Descriptive `<title>` on every page | Yes |52| 2.4.3 Focus Order | Logical tab order | Manual |53| 2.4.4 Link Purpose | Link text describes destination (no "click here") | Partial |54| 2.4.6 Headings and Labels | Descriptive headings, no skipped levels | Yes |55| 2.4.7 Focus Visible | Visible focus indicator on all focusable elements | Partial |56| **2.4.11 Focus Not Obscured** (2.2) | Focused element not fully hidden by other content | Manual |57| **2.4.13 Focus Appearance** (2.2, **AAA** — house rule, not required for AA) | Focus indicator has 3:1 contrast, 2px minimum | Partial |58| **2.5.7 Dragging Movements** (2.2) | Drag operations have single-pointer alternative | Manual |59| **2.5.8 Target Size** (2.2) | Interactive targets minimum 24x24px (44x44px mobile) | Yes |6061### Principle 3: Understandable6263Information and UI operation must be understandable.6465| Criterion | What to Check | Automated? |66|-----------|---------------|-----------|67| 3.1.1 Language of Page | `lang` attribute on `<html>` element | Yes |68| 3.1.2 Language of Parts | `lang` attribute on foreign language content | Manual |69| 3.2.1 On Focus | No context change on focus alone | Manual |70| 3.2.2 On Input | No unexpected context change on input | Manual |71| 3.3.1 Error Identification | Errors described in text, not just color | Manual |72| 3.3.2 Labels or Instructions | Form inputs have labels; complex inputs have instructions | Yes |73| **3.2.6 Consistent Help** (2.2) | Help mechanisms in same relative location across pages | Manual |74| **3.3.7 Redundant Entry** (2.2) | Previously entered info auto-populated or selectable | Manual |75| **3.3.8 Accessible Authentication** (2.2) | No cognitive function test for auth (allow paste, managers) | Manual |7677### Principle 4: Robust7879Content must be robust enough for assistive technologies.8081| Criterion | What to Check | Automated? |82|-----------|---------------|-----------|83| 4.1.2 Name, Role, Value | Custom components have proper ARIA roles/states | Partial |84| 4.1.3 Status Messages | Dynamic content uses `aria-live` regions | Partial |8586## Automated Checks8788Run these checks programmatically:8990### Code-Level (Grep/Read)91```92Check Pattern to Search93───────────────────────────── ──────────────────────────────94Missing alt text <img without alt=95Div as button <div.*onClick (without role="button")96Missing form labels <input without associated <label>97Empty links <a>...</a> with no text content98Missing lang attribute <html without lang=99Heading hierarchy h1, h2, h3 order (no skipped levels)100Focus outline removed outline: none without replacement101Hardcoded color as info Color-only status indicators102```103104### Token contrast is computable — do not send it to a browser105106**Audit the tokens at their source, not only the rendered page.** A design token pair is two static107HSL values in a file; the WCAG ratio between them is arithmetic, so it belongs in a test, not in a108manual DevTools pass. Nobody opens DevTools 36 times, which is exactly how a contrast failure109survives: not because it is subtle, but because checking it by hand does not scale and so does not110happen.111112Every `--x` / `--x-foreground` pair in `globals.css` (or wherever the tokens live) is a case:113114```ts115// The formula and a ready-made contrastRatio() live in116// @skills/std-design-system/references/defining-tokens.md — import, do not re-derive.117import { contrastRatio } from './check-contrast';118import { light, dark } from './tokens';119120// Iterate the PAIRS — do not hand-write two cases and call it covered.121const PAIRS = ['primary', 'success', 'error', 'warning', 'info'] as const;122123describe.each([['light', light], ['dark', dark]])('%s token contrast', (_name, theme) => {124 it.each(PAIRS)('%s-foreground on %s clears 4.5:1', (token) => {125 expect(contrastRatio(theme[`${token}Foreground`], theme[token])).toBeGreaterThanOrEqual(4.5);126 });127 it('muted-foreground on muted clears 4.5:1', () => {128 expect(contrastRatio(theme.mutedForeground, theme.muted)).toBeGreaterThanOrEqual(4.5);129 });130});131```132133Two traps this catches that a page-level audit does not:134135- **A `-foreground` token is verified against its SOLID surface.** `bg-success/10136 text-success-foreground` is a 10% tint under near-white text — around 1:1, invisible — and no137 static check of the *token pair* sees it, because the pair itself is fine. On a tint, use138 `text-foreground`.139- **Presets and themes are copies.** If the project ships more than one theme, every theme is a140 case. A fix applied to the spec does not reach the preset beside it.141142### Tool-Based143| Tool | Purpose | Integration |144|------|---------|-------------|145| `axe-core` / `jest-axe` | Automated WCAG checks in Vitest | `npm run test` |146| Token contrast test (above) | Every token pair, every theme | `npm run test` — **not manual** |147| Chrome DevTools | Accessibility tree, live contrast spot-checks | Manual |148| VoiceOver (macOS) | Screen reader testing | Manual |149| NVDA (Windows) | Screen reader testing | Manual |150| Lighthouse | Accessibility score | CI pipeline |151152## Output Format153154Present findings as:155156```157[SEVERITY] | File:Line | WCAG Criterion | Finding | Remediation158───────────┼──────────┼────────────────┼─────────┼────────────159CRITICAL | path:42 | 2.4.7 | No focus indicator on submit button | Add focus-visible:ring-2 focus-visible:ring-ring160MAJOR | path:15 | 1.1.1 | <img> missing alt text | Add descriptive alt or alt="" for decorative161MINOR | path:88 | 2.4.4 | Link text is "click here" | Use descriptive text: "View order details"162```163164**Severity Levels:**165- **CRITICAL**: Blocks user access entirely (no keyboard access, missing essential alt text)166- **MAJOR**: Significantly impairs experience (low contrast, missing labels)167- **MINOR**: Degrades experience but workaround exists (non-descriptive link text)168- **INFO**: Best practice recommendation (could improve but compliant)169170## Summary Metrics171172End each audit with:173174| Metric | Value |175|--------|-------|176| Total checks performed | X |177| CRITICAL issues | X |178| MAJOR issues | X |179| MINOR issues | X |180| Estimated compliance | X% (passing checks / total checks) |181| WCAG 2.2 AA conformance | Yes / No (any CRITICAL = No) |182183## Full References184185- `references/wcag-22-checklist.md` — Complete WCAG 2.2 AA checklist by principle186- `references/aria-patterns.md` — ARIA widget patterns with keyboard specifications187188### Owned elsewhere189190- **`std-accessibility`** is the WCAG rule set — it is **scoped to `.tsx`/`.jsx`** under191 `src/`, `app/`, and `components/`. Note the edge it does *not* cover: its `paths:` are component192 files, so it does **not** load on `globals.css` or a token file. Contrast decisions get made193 where the tokens are defined, and that is `std-design-system`'s territory (it is scoped to194 `**/globals.css`, `**/styles/**`, `**/tailwind.config.*`). Auditing a colour system means opening195 both.196- **`@skills/std-design-system/references/defining-tokens.md`** — the `contrastRatio()`197 implementation, the Vitest wiring, and the rule for *which* side to fix: darken the **foreground**198 for a brand token (the surface is the brand decision), darken the **surface** for a semantic199 status token (which green is not the convention; "green means success" is).200- **`@skills/theming/references/design-tokens.md`** — the canonical token values and their measured201 ratios. **`@skills/theming/references/theme-presets.md`** — three presets meant to be copied202 wholesale, which is why each is audited as its own theme.203- `accessibility-checker.py` warns on `.tsx`/`.jsx`/`.css`/`.scss` (semantic HTML, alt text, label204 association, focus indicators, ARIA misuse). It does **not** compute contrast — that is the test205 above, because the ratio needs the token values, not the markup.