skills/accessibility-testing/SKILL.md
Automated accessibility testing and WCAG compliance validation. Use when: adding accessibility tests to a project, configuring axe-core or pa11y, integrating a11y checks into CI, auditing an existing application for accessibility violations, validating ARIA usage, testing keyboard navigation, or building an accessibility regression suite. Covers axe-core, Playwright a11y testing, Lighthouse accessibility audits, CI integration, and WCAG 2.2 AA compliance.
npx skillsauth add michaelsvanbeek/personal-agent-skills accessibility-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Automated tools catch ~30-40% of WCAG violations (missing alt text, low contrast, invalid ARIA). The rest requires manual testing with screen readers and keyboard navigation. Automate the detectable, then build manual test plans for the rest.
| Layer | Tool | Catches | Speed | |-------|------|---------|-------| | Static analysis | eslint-plugin-jsx-a11y | Missing alt, invalid ARIA in JSX | Instant (lint) | | Component tests | @axe-core/react, vitest-axe | Per-component violations | Fast (unit) | | Integration tests | Playwright + axe-core | Full-page violations, focus flow | Medium | | Audit | Lighthouse CI | WCAG score, best practices | Slow (CI) | | Manual | Screen reader + keyboard | Context, flow, comprehension | Slowest |
Rule: Start from the top. Catch what you can at lint time before it reaches tests.
npm install -D eslint-plugin-jsx-a11y
eslint.config.ts)import jsxA11y from "eslint-plugin-jsx-a11y";
export default [
// ... other config
jsxA11y.flatConfigs.recommended,
];
alt on <img>htmlFor on <label>role and keyboard support)lang on <html>npm install -D @axe-core/react vitest-axe jsdom
import { render } from "@testing-library/react";
import { axe, toHaveNoViolations } from "vitest-axe";
import { expect, test } from "vitest";
import { MyComponent } from "./MyComponent";
expect.extend(toHaveNoViolations);
test("MyComponent has no accessibility violations", async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
axe test for every major component (forms, modals, navigation, tables).npm install -D @playwright/test @axe-core/playwright
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
test("homepage has no accessibility violations", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag22aa"])
.analyze();
expect(results.violations).toEqual([]);
});
test("checkout form is accessible", async ({ page }) => {
await page.goto("/checkout");
const results = await new AxeBuilder({ page })
.include("#checkout-form")
.withTags(["wcag2a", "wcag2aa"])
.analyze();
expect(results.violations).toEqual([]);
});
test("modal can be operated with keyboard only", async ({ page }) => {
await page.goto("/");
await page.click('[data-testid="open-modal"]');
// Focus should be trapped in modal
const modal = page.locator('[role="dialog"]');
await expect(modal).toBeFocused();
// Tab through focusable elements
await page.keyboard.press("Tab");
await expect(page.locator('[data-testid="modal-input"]')).toBeFocused();
// Escape closes the modal
await page.keyboard.press("Escape");
await expect(modal).not.toBeVisible();
// Focus returns to trigger element
await expect(page.locator('[data-testid="open-modal"]')).toBeFocused();
});
withTags(["wcag2a", "wcag2aa", "wcag22aa"]) to target the correct WCAG level.npm install -D @lhci/cli
lighthouserc.js)module.exports = {
ci: {
collect: {
url: ["http://localhost:3000/", "http://localhost:3000/dashboard"],
startServerCommand: "npm run preview",
numberOfRuns: 3,
},
assert: {
assertions: {
"categories:accessibility": ["error", { minScore: 0.9 }],
"color-contrast": "error",
"image-alt": "error",
"label": "error",
"link-name": "error",
"button-name": "error",
},
},
upload: {
target: "temporary-public-storage",
},
},
};
- name: lighthouse-a11y
image: node:22-alpine
commands:
- npm ci
- npm run build
- npx @lhci/cli autorun
color-contrast, image-alt, label, link-name, button-name) are always errors.The most commonly violated WCAG criteria:
alt text (or alt="" for decorative images)outline: none without replacement)prefers-reduced-motion media query<html lang="..."> is set on every page<label> elementsrole="status" or aria-live regions| OS | Screen Reader | Browser | |----|--------------|---------| | macOS | VoiceOver (built-in) | Safari | | Windows | NVDA (free) | Firefox or Chrome | | Windows | JAWS (paid) | Chrome |
| axe-core Impact | Action | |----------------|--------| | critical | Fix before merge. Blocks users entirely. | | serious | Fix before merge. Significant barrier. | | moderate | Fix within current sprint. Degraded experience. | | minor | Add to backlog. Low-impact inconvenience. |
When a rule must be disabled (rare), document it:
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.disableRules(["color-contrast"]) // Known issue: #1234, tracking upstream fix
.analyze();
Rule: Never disable a rule without a tracking issue and a comment explaining why.
| Anti-Pattern | Problem | Fix |
|-------------|---------|-----|
| aria-label on everything | Overrides visible text, confuses screen readers | Use visible labels; ARIA is a last resort |
| outline: none without replacement | Keyboard users can't see focus | Use :focus-visible with custom styles |
| role="button" on <div> | Missing keyboard support, missing semantics | Use <button> element instead |
| Testing only with automation | Misses flow, context, and comprehension issues | Pair automation with manual screen reader testing |
| Fixing tests instead of components | Violations persist for users | Fix the component, then verify the test passes |
| tabindex="1" or higher | Breaks natural focus order | Only use tabindex="0" or tabindex="-1" |
When auditing an existing project for accessibility testing:
prefers-reduced-motion is respected for animationsdevelopment
TypeScript coding standards and type safety conventions. Use when: creating TypeScript files, defining interfaces and types, writing type-safe code, reviewing TypeScript for type correctness, auditing a codebase for type safety gaps, eliminating any or ts-ignore usage, or improving strict-mode compliance. Covers strict typing, avoiding any and ts-ignore, discriminated unions, Zod runtime validation, immutability patterns, and proper type definitions.
testing
Writing clear, actionable tickets in any issue tracker (Jira, Linear, GitHub Issues, ServiceNow, etc.). Use when: creating epics, stories, tasks, bugs, or spikes; writing acceptance criteria; decomposing work for a sprint; linking dependencies between tickets; auditing backlog items for clarity; or coaching a team on ticket quality. Covers title conventions, description templates, acceptance criteria, decomposition rules, dependency linking, and org-specific pluggable configuration.
development
Testing strategy, patterns, and evaluation for software and LLM/AI systems. Use when: writing tests, choosing test boundaries, designing test data, structuring test suites, evaluating LLM outputs, building evaluation pipelines, setting coverage thresholds, auditing test coverage gaps in existing projects, or improving test quality and structure.
development
Writing effective status updates for different audiences and cadences. Use when: writing a weekly status update, preparing a monthly summary, drafting a quarterly review, sending updates to leadership, sharing progress with stakeholders, or improving the clarity and impact of team communications. Covers weekly, monthly, and quarterly formats tailored for upward, lateral, and downward communication.