Agent Skills: axe-playwright

Automated accessibility tests with @axe-core/playwright (AxeBuilder) inside @playwright/test. Use when adding or debugging a11y specs, WCAG tag filtering, scoping scans with include/exclude, disabling rules, scanning light and dark themes, per-route scans, or interpreting color-contrast results on canvas/WebGL, gradients and overlapped elements.

UncategorizedID: enderpuentes/ai-agent-skills/axe-playwright

Install this agent skill to your local

pnpm dlx add-skill https://github.com/EnderPuentes/ai-agent-skills/tree/HEAD/axe-playwright

Skill Files

Browse the full folder contents for axe-playwright.

Download Skill

Loading file tree…

axe-playwright/SKILL.md

Skill Metadata

Name
axe-playwright
Description
Automated accessibility tests with @axe-core/playwright (AxeBuilder) inside @playwright/test. Use when adding or debugging a11y specs, WCAG tag filtering, scoping scans with include/exclude, disabling rules, scanning light and dark themes, per-route scans, or interpreting color-contrast results on canvas/WebGL, gradients and overlapped elements.

axe-playwright

Axe finds only part of the WCAG failures. Treat a green scan as "no detectable violations", not as compliance. Keyboard, focus order and screen reader checks stay manual.

Setup

npm i -D @playwright/test @axe-core/playwright
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

Create a new AxeBuilder({ page }) per scan. Do not reuse a builder across pages.

Basic scan

test('home has no a11y violations', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

Builder methods (verified against the package README)

  • include(selector) and exclude(selector) scope the scan. Both chain.
  • withTags(string | string[]) limits rules by tag.
  • withRules(string | string[]) runs only those rule ids.
  • disableRules(string | string[]) skips rule ids.
  • options(axeRunOptions) passes raw axe.run options.
  • setLegacyMode(boolean) toggles legacy frame testing.
  • analyze() returns the results object.

Tags

Use ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] for the usual AA target. Without withTags, axe also runs best-practice rules, which fail builds on non-WCAG items.

new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();

Scoping

new AxeBuilder({ page }).include('main').exclude('[data-testid="3d-canvas"]').analyze();

Prefer exclude for third-party widgets and disableRules only for a known issue with a tracked ticket. Leave a comment with the ticket id beside it.

Known limits

  • color-contrast cannot read pixels from <canvas> or WebGL. Text drawn inside them is not checked, and DOM text over them is reported as "incomplete", not as a violation.
  • Gradient, image and semi-transparent backgrounds usually land in results.incomplete. Review these by hand or with a contrast tool on a screenshot.
  • Overlapped elements (sticky nav, modals, pseudo-element overlays) produce incomplete results for the covered text.
  • Animations in flight give unstable colors. Wait for them to settle or emulate reduced motion before scanning.
  • Axe sees the DOM state at scan time. Open menus, dialogs and tabs must be opened first.

Always assert on violations, and log incomplete so skipped contrast checks stay visible.

Themes

Switch theme first, then scan. Match how the app stores it.

for (const scheme of ['light', 'dark'] as const) {
  test(`home a11y (${scheme})`, async ({ page }) => {
    await page.emulateMedia({ colorScheme: scheme });
    await page.goto('/');
    const results = await new AxeBuilder({ page }).withTags(['wcag2aa']).analyze();
    expect(results.violations).toEqual([]);
  });
}

If the theme is a class or attribute (next-themes), set it with page.addInitScript writing localStorage, or click the toggle, then wait for the attribute before scanning.

Per-route scans

const routes = ['/', '/en/notes', '/es/notes'];
for (const route of routes) {
  test(`a11y ${route}`, async ({ page }) => {
    await page.goto(route);
    const results = await new AxeBuilder({ page }).analyze();
    expect(results.violations).toEqual([]);
  });
}

Reporting in CI

Attach the full results so failures can be inspected from the HTML report.

test('scan', async ({ page }, testInfo) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page }).analyze();
  await testInfo.attach('accessibility-scan-results', {
    body: JSON.stringify(results, null, 2),
    contentType: 'application/json',
  });
  const summary = results.violations.map(v => `${v.id} (${v.impact}) x${v.nodes.length}: ${v.nodes[0].target.join(' ')}`);
  expect(summary).toEqual([]);
});

Asserting on a mapped summary makes the failure diff readable. Wrap the setup in a fixture (test.extend) to share tags and excludes across specs.

Author: Ender Puentes (with Claude Code) Source: https://playwright.dev/docs/accessibility-testing