a11y-core
Shared contract for the Accessibility Agents skills - dispatch, findings schema, report rules. Read by skills, never dispatched…
Reference data, not a reviewer. Browser accessibility testing using Playwright and @axe-core/playwright. Keyboard scans, contrast verification, and accessibility tree snapshots.
Claude Code
$ npx -y skills add Community-Access/accessibility-agents --skill kb-playwright-testing --agent claude-code
Codex
$ npx skills add Community-Access/accessibility-agents --skill kb-playwright-testing -a codex -y
Cursor
$ npx skills add Community-Access/accessibility-agents --skill kb-playwright-testing -a cursor -y
Gemini CLI
$ npx skills add Community-Access/accessibility-agents --skill kb-playwright-testing -a gemini-cli -y
GitHub Copilot
$ npx skills add Community-Access/accessibility-agents --skill kb-playwright-testing -a github-copilot -y
OpenCode
$ npx skills add Community-Access/accessibility-agents --skill kb-playwright-testing -a opencode -y
Windsurf
$ npx skills add Community-Access/accessibility-agents --skill kb-playwright-testing -a windsurf -y
name: kb-playwright-testing description: "Reference data, not a reviewer. Browser accessibility testing using Playwright and @axe-core/playwright. Keyboard scans, contrast verification, and accessibility tree snapshots." license: MIT disable-model-invocation: true user-invocable: false metadata: tier: reference domain: cross-cutting output: none effort: low title: Playwright Testing
Reusable knowledge module for browser-based accessibility testing using Playwright and @axe-core/playwright.
Each tool the MCP server registers (`mcp-server/tools/playwright-tools.js`), with its purpose and whether it needs @axe-core/playwright. All of them need Playwright with Chromium. There is no state-scan tool: to scan content revealed by an interaction, script the interaction and run `AxeBuilder` yourself (see Scan After Interaction).
| Tool | Purpose | Requires @axe-core/playwright | |------|---------|------------------------------| | `run_axe_scan` | axe-core scan of a URL; default tags `wcag2a, wcag2aa, wcag21a, wcag21aa, wcag22aa` | Yes | | `run_playwright_keyboard_scan` | Tab-order traversal, focus visibility, keyboard trap detection | No | | `run_playwright_viewport_scan` | Horizontal scrolling at 320, 768, 1024 and 1280 CSS px (1.4.10 Reflow only; no zoom, text resize or target-size check) | No | | `run_playwright_contrast_scan` | Computed-style text contrast against the AA thresholds (4.5:1, or 3:1 for large text) | No | | `run_playwright_a11y_tree` | Browser accessibility tree snapshot (`ariaSnapshot`) | No |
If the scanner tools are not available, ask for the results from axe or Accessibility Insights, use only what you are given, and never say you scanned, ran or opened anything you did not.
import AxeBuilder from '@axe-core/playwright';
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
.analyze();In axe-core 4.14 the only `wcag22aa` rule is `target-size`, which runs only when that tag is listed. Most WCAG 2.2 criteria (2.4.11, 2.5.7, 3.2.6, 3.3.7, 3.3.8) have no axe rule and need manual or scripted checks. `AxeBuilder` needs a page opened from a browser context; the Playwright Test `page` fixture is one, `browser.newPage()` is not.
const results = await new AxeBuilder({ page })
.include('.modal-content')
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
.analyze();const results = await new AxeBuilder({ page })
.include('#hero-image')
.withRules(['image-alt'])
.analyze();
expect(results.violations).toEqual([]);await page.locator('[aria-expanded="false"]').first().click();
await page.locator('.accordion-content').first().waitFor({ state: 'visible' });
const results = await new AxeBuilder({ page })
.include('.accordion-content')
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
.analyze();const tabStops = [];
for (let i = 0; i < maxTabs; i++) {
await page.keyboard.press('Tab');
const info = await page.evaluate(() => {
const el = document.activeElement;
return {
tagName: el?.tagName,
role: el?.getAttribute('role'),
name: el?.getAttribute('aria-label') || el?.textContent?.trim().slice(0, 50),
id: el?.id,
tabIndex: el?.tabIndex
};
});
tabStops.push(info);
}A keyboard trap is detected when the same element receives focus after consecutive Tab presses:
let trapCount = 0;
let lastSelector = '';
for (const stop of tabStops) {
const currentSelector = `${stop.tagName}#${stop.id}`;
if (currentSelector === lastSelector) {
trapCount++;
if (trapCount >= 3) { /* TRAP DETECTED */ }
} else {
trapCount = 0;
}
lastSelector = currentSelector;
}await page.locator('[data-modal-trigger]').click();
await page.getByRole('dialog').waitFor({ state: 'visible' });
const focusedRole = await page.evaluate(() =>
document.activeElement?.closest('[role="dialog"]') ? 'inside-dialog' : 'outside-dialog'
);
// focusedRole should be 'inside-dialog'test('modal traps focus correctly', async ({ page }) => {
await page.goto(url);
await page.locator('[data-open-modal]').click();
await expect(page.getByRole('dialog')).toBeVisible();
// Focus should be inside the dialog
const inDialog = await page.evaluate(() =>
document.activeElement?.closest('[role="dialog"]') !== null
);
expect(inDialog).toBe(true);
// Tab through dialog - should not escape
for (let i = 0; i < 20; i++) {
await page.keyboard.press('Tab');
const stillInDialog = await page.evaluate(() =>
document.activeElement?.closest('[role="dialog"]') !== null
);
expect(stillInDialog).toBe(true);
}
// Escape should close and return focus to trigger
await page.keyboard.press('Escape');
const focusedId = await page.evaluate(() => document.activeElement?.id);
expect(focusedId).toBe('modal-trigger-id');
});test('skip link moves focus to main content', async ({ page }) => {
await page.goto(url);
await page.keyboard.press('Tab'); // Focus skip link
await page.keyboard.press('Enter'); // Activate it
const focusedId = await page.evaluate(() => document.activeElement?.id);
expect(focusedId).toBe('main-content');
});name: Accessibility Tests
on: [push, pull_request]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/seWCAG 2.2 AA enforcement for agentic coding, as a set of Agent Skills. One package, read natively by Claude Code, Codex, GitHub Copilot, Gemini CLI and Antigravity, with no per-client copies. Models forget accessibility while generating code.
Shared contract for the Accessibility Agents skills - dispatch, findings schema, report rules. Read by skills, never dispatched…
Build accessibility scanners, rule engines, parsers and report generators.
Web UI accessibility lead. Use before writing or changing HTML, JSX, TSX, Vue, Svelte, CSS or templates. Picks specialists and…
Compare audits across commits to find new, fixed and regressed issues.
Generate a W3C or EU model accessibility statement from audit results.
GitHub Actions: workflow runs, logs, re-runs and CI failure triage.
Produce the "Playwright 1.xx API Review" artifact for a range of commits - the public API additions and the breaking or behavior…
Write, refine, run, and QA promptfoo evaluation suites: promptfooconfig.yaml, prompts, providers, vars, tests, assertions,…
DeepEval evaluation workflow for AI agents and LLM applications. TRIGGER when the user wants to evaluate or improve an AI agent,…