Accessibility testing for web applications using Playwright (@playwright/test), TypeScript, and axe-core. Use to write, run, or debug WCAG 2.2 AA checks, keyboard and focus tests, ARIA/semantic validation, accessible names, form labels, color contrast, or screen-reader test patterns. Keywords: accessibility, WCAG, axe-core, keyboard navigation, focus management, ARIA.
npx skills add https://github.com/fugazi/test-automation-skills-agents --skill a11y-playwright-testing
Comprehensive toolkit for automated accessibility testing using Playwright with TypeScript and axe-core. Enables WCAG 2.2 Level AA compliance verification (superset of 2.1), keyboard operability testing, semantic validation, and accessibility regression prevention.
> Activation: This skill is triggered when working with accessibility testing, WCAG compliance, axe-core scans, keyboard navigation tests, focus management, ARIA validation, or screen reader compatibility.
accessibility-selenium-testing).playwright-e2e-testing).| Requirement | Details |
| ----------- | ------------------------------ |
| Node.js | v18+ recommended |
| Playwright | @playwright/test installed |
| axe-core | @axe-core/playwright package |
| TypeScript | Configured in project |
# Add axe-core to existing Playwright project
npm install -D @axe-core/playwright axe-core
Before writing accessibility tests, clarify:
> [!] Critical: Automated tooling can detect ~30-40% of accessibility issues. Use automation to prevent regressions and catch common failures; manual audits are required for full WCAG conformance.
Prefer native HTML semantics over ARIA. Use ARIA only when native elements cannot achieve the required semantics.
// [ok] Semantic HTML - inherently accessible
await page.getByRole("button", { name: "Submit" }).click();
// [no] ARIA override - requires manual keyboard/focus handling
await page.locator('[role="button"]').click(); // Often a <div>
If you cannot locate an element by role or label, it's often an accessibility defect.
| Locator Success | Accessibility Signal |
| -------------------------------------------- | -------------------------- |
| getByRole('button', { name: 'Submit' }) [ok] | Button has accessible name |
| getByLabel('Email') [ok] | Input properly labeled |
| getByRole('navigation') [ok] | Landmark exists |
| locator('.submit-btn') [!] | May lack accessible name |
import AxeBuilder from "@axe-core/playwright";
import { test, expect } from "@playwright/test";
test("page has no WCAG 2.2 AA violations", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22a", "wcag22aa"])
.analyze();
expect(results.violations).toEqual([]);
});
test("form component is accessible", async ({ page }) => {
await page.goto("/contact");
const results = await new AxeBuilder({ page })
.include("#contact-form") // Scope to specific component
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22a", "wcag22aa"])
.analyze();
expect(results.violations).toEqual([]);
});
test("form is keyboard navigable", async ({ page }) => {
await page.goto("/login");
// Tab to first field
await page.keyboard.press("Tab");
await expect(page.getByLabel("Email")).toBeFocused();
// Tab to password
await page.keyboard.press("Tab");
await expect(page.getByLabel("Password")).toBeFocused();
// Tab to submit button
await page.keyboard.press("Tab");
await expect(page.getByRole("button", { name: "Sign in" })).toBeFocused();
// Submit with Enter
await page.keyboard.press("Enter");
await expect(page).toHaveURL(/dashboard/);
});
test("dialog traps and returns focus", async ({ page }) => {
await page.goto("/settings");
const trigger = page.getByRole("button", { name: "Delete account" });
// Open dialog
await trigger.click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
// Focus should be inside dialog
await expect(dialog.getByRole("button", { name: "Cancel" })).toBeFocused();
// Tab should stay trapped in dialog
await page.keyboard.press("Tab");
await expect(dialog.getByRole("button", { name: "Confirm" })).toBeFocused();
await page.keyboard.press("Tab");
await expect(dialog.getByRole("button", { name: "Cancel" })).toBeFocused();
// Escape closes and returns focus to trigger
await page.keyboard.press("Escape");
await expect(dialog).toBeHidden();
await expect(trigger).toBeFocused();
});
test("skip link moves focus to main content", async ({ page }) => {
await page.goto("/");
// First Tab should focus skip link
await page.keyboard.press("Tab");
const skipLink = page.getByRole("link", { name: /skip to (main|content)/i });
await expect(skipLink).toBeFocused();
// Activating skip link moves focus to main
await page.keyboard.press("Enter");
await expect(page.locator('#main, [role="main"]').first()).toBeFocused();
});
| Principle | Focus Areas | Example Tests |
| ------------------ | ----------------------------------------- | ------------------------------------------------- |
| Perceivable | Alt text, captions, contrast, structure | Image alternatives, color contrast ratio |
| Operable | Keyboard, focus, timing, navigation | Tab order, focus visibility, skip links |
| Understandable | Labels, instructions, errors, consistency | Form labels, error messages, predictable behavior |
| Robust | Valid HTML, ARIA, name/role/value | Semantic structure, accessible names |
Default: wcag2a, wcag2aa, wcag21a, wcag21aa, wcag22a, wcag22aa (WCAG 2.2 AA). Use best-practice for additional checks. See references/axe-tags-reference.md for full tag list.
When exceptions are unavoidable:
// [no] Avoid: Global rule disable
new AxeBuilder({ page }).disableRules(["color-contrast"]);
// [ok] Better: Scoped exclusion with documentation
new AxeBuilder({ page })
.exclude("#third-party-widget") // Known issue: JIRA-1234, fix by Q2
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22a", "wcag22aa"])
.analyze();
| Problem | Cause | Solution |
| ------------------------------------------------- | ------------------------------ | --------------------------------------- |
| Axe finds 0 violations but app fails manual audit | Automation covers ~30-40% | Add manual testing checklist |
| False positive on dynamic content | Content not fully rendered | Wait for stable state before scan |
| Color contrast fails incorrectly | Background image/gradient | Use exclude for known false positives |
| Cannot find element by role | Missing semantic HTML | Fix markup - this is a real bug |
| Focus not visible | Missing :focus styles | Add visible focus indicator CSS |
| Dialog focus not trapped | Missing focus trap logic | Implement focus trap (see snippets) |
| Skip link doesn't work | Target missing tabindex="-1" | Add tabindex to main content |
| Command | Description |
| ----------------------------------- | -------------------------------------- |
| npx playwright test --grep "a11y" | Run accessibility tests only |
| npx playwright test --headed | Run with visible browser for debugging |
| npx playwright test --debug | Step through with Inspector |
| PWDEBUG=1 npx playwright test | Debug mode with pause |
color-contrast) instead of scoped .exclude() with a documented ticket.| Document | Content |
| -------------------------------------------------------------------------------- | ------------------------------------------------ |
| Snippets: Setup & Scanning | axe-core setup, helper, and scanning patterns |
| Snippets: Keyboard, Focus, Semantic | Keyboard navigation, focus management, semantic structure |
| Snippets: Visual, Names, Checklist | Visual accessibility, accessible names, critical pages |
| WCAG 2.2 AA Checklist | Manual audit checklist by POUR principle |
| ARIA Patterns: Widgets Part 1 | Fundamentals, dialog, tabs, menu widgets |
| ARIA Patterns: Widgets Part 2 | Accordion, combobox, live regions, tooltip |
| ARIA Patterns: Mistakes & Reference | Common ARIA mistakes and roles quick reference |
| Resource | URL |
| ---------------------------- | --------------------------------------- |
| WCAG 2.2 Specification | https://www.w3.org/TR/WCAG22/ |
| WCAG Quick Reference | https://www.w3.org/WAI/WCAG22/quickref/ |
| WAI-ARIA Authoring Practices | https://www.w3.org/WAI/ARIA/apg/ |
| axe-core Rules | https://dequeuniversity.com/rules/axe/ |
AxeBuilder.analyze() returns zero critical violationswcag22a/wcag22aa included in scans (focus-not-obscured, dragging movements, target-size minimums)Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
Run Playwright tests at scale with cloud-hosted browsers and integrated Azure portal reporting.
Browser debugging, performance profiling, and automation via Chrome DevTools MCP. Use when user says "debug this page", "take a screenshot", "check network requests", "profile performance", "inspect console errors", or "analyze page load". Do NOT use for full E2E test suites (use playwright-skill) or non-browser debugging.
QA-test a website or web app and return a 1-5 quality score (5 = flawless, 1 = broken) with evidence. Use when the user wants to test, QA, evaluate, score, or "check how good" a site, page, flow, or app — including a local dev server (e.g. "qa test localhost:5173", "does the checkout work?", "rate this landing page"). Drives a real Browser Use cloud browser, tunneling localhost automatically.
Set up component testing with Playwright using a story gallery — scaffold stories and a gallery dev page driven by the built-in mount fixture, no dedicated component-testing runtime. Use when asked to test React or Vue components in isolation with Playwright, or to migrate off @playwright/experimental-ct-react / -vue.
Tests in real browsers via Chrome DevTools MCP. Use when building or debugging anything that runs in a browser. Use when you need to inspect the DOM, capture console errors, analyze network requests, profile performance, or verify visual output with real runtime data. Requires the chrome-devtools MCP server to be configured.
Always use browser-harness for any web interaction: automation, scraping, testing, or site/app work.
Run a browser-based UI review of the WordPress.com Help Center across multiple surfaces, looking for visual and behavioral issues. Use when asked to test the Help Center UI.
Take fugazi/a11y-playwright-testing from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npm, npx.
Without those the skill loads but fails at the first command.