skills/a11y-playwright-testing/SKILL.md
Accessibility testing for web applications using Playwright (@playwright/test) with TypeScript and axe-core. Use when asked to write, run, or debug automated accessibility checks, keyboard navigation tests, focus management, ARIA/semantic validations, screen reader compatibility, or WCAG 2.1 Level AA compliance testing. Covers axe-core integration, POUR principles (perceivable, operable, understandable, robust), color contrast, form labels, landmarks, and accessible names.
npx skillsauth add jyjeanne/ai-setup-forge a11y-playwright-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.
Comprehensive toolkit for automated accessibility testing using Playwright with TypeScript and axe-core. Enables WCAG 2.1 Level AA compliance verification, 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.
| 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.
// ✅ Semantic HTML - inherently accessible
await page.getByRole('button', { name: 'Submit' }).click();
// ❌ 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' }) ✅ | Button has accessible name |
| getByLabel('Email') ✅ | Input properly labeled |
| getByRole('navigation') ✅ | 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.1 AA violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.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'])
.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 |
| Tag | WCAG Level | Use Case |
|-----|------------|----------|
| wcag2a | Level A | Minimum compliance |
| wcag2aa | Level AA | Standard target |
| wcag2aaa | Level AAA | Enhanced (rarely full) |
| wcag21a | 2.1 Level A | WCAG 2.1 specific A |
| wcag21aa | 2.1 Level AA | WCAG 2.1 standard |
| best-practice | Beyond WCAG | Additional recommendations |
const WCAG21AA_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];
When exceptions are unavoidable:
// ❌ Avoid: Global rule disable
new AxeBuilder({ page }).disableRules(['color-contrast']);
// ✅ Better: Scoped exclusion with documentation
new AxeBuilder({ page })
.exclude('#third-party-widget') // Known issue: JIRA-1234, fix by Q2
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.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 |
| Document | Content | |----------|---------| | Snippets | axe-core setup, helpers, keyboard/focus patterns | | WCAG 2.1 AA Checklist | Manual audit checklist by POUR principle | | ARIA Patterns | Common ARIA widget patterns and validations |
| Resource | URL | |----------|-----| | WCAG 2.1 Specification | https://www.w3.org/TR/WCAG21/ | | WCAG Quick Reference | https://www.w3.org/WAI/WCAG21/quickref/ | | WAI-ARIA Authoring Practices | https://www.w3.org/WAI/ARIA/apg/ | | axe-core Rules | https://dequeuniversity.com/rules/axe/ |
development
Generate breadboard circuit mockups and visual diagrams using HTML5 Canvas drawing techniques. Use when asked to create circuit layouts, visualize electronic component placements, draw breadboard diagrams, mockup 6502 builds, generate retro computer schematics, or design vintage electronics projects. Supports 555 timers, W65C02S microprocessors, 28C256 EEPROMs, W65C22 VIA chips, 7400-series logic gates, LEDs, resistors, capacitors, switches, buttons, crystals, and wires.
development
Apply lean thinking to UX: hypothesis-driven design, collaborative sketching, and rapid experiments instead of heavy deliverables. Use when the user mentions "Lean UX", "design hypothesis", "UX experiment", "collaborative design", or "outcome over output". Covers hypothesis statements, MVPs for UX, and cross-functional collaboration. For Build-Measure-Learn, see lean-startup. For usability audits, see ux-heuristics.
development
Design MVPs, validated learning experiments, and pivot-or-persevere decisions using Build-Measure-Learn. Use when the user mentions "MVP scope", "validated learning", "pivot or persevere", "vanity metrics", or "test assumptions". Covers innovation accounting and actionable metrics. For 5-day prototype testing, see design-sprint. For customer motivation analysis, see jobs-to-be-done.
tools
Instrument, trace, evaluate, and monitor LLM applications and AI agents with LangSmith. Use when setting up observability for LLM pipelines, running offline or online evaluations, managing prompts in the Prompt Hub, creating datasets for regression testing, or deploying agent servers. Triggers on: langsmith, langchain tracing, llm tracing, llm observability, llm evaluation, trace llm calls, @traceable, wrap_openai, langsmith evaluate, langsmith dataset, langsmith feedback, langsmith prompt hub, langsmith project, llm monitoring, llm debugging, llm quality, openevals, langsmith cli, langsmith experiment, annotate llm, llm judge.