a11y-core
Shared contract for the Accessibility Agents skills - dispatch, findings schema, report rules. Read by skills, never dispatched on its own.
Reference data, not a reviewer. Browser accessibility testing using Playwright and @axe-core/playwright. Keyboard scans, contrast verification, and accessibility tree snapshots.
$ npx -y skills add Community-Access/accessibility-agents --skill kb-playwright-testing --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/kb-playwright-testingContext preview
The summary Claude sees to decide when to auto-load this skill.
Reference data, not a reviewer. Browser accessibility testing using Playwright and @axe-core/playwright. Keyboard scans, contrast verification, and accessibility tree snapshots.
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, with its purpose and requires @axe-core or playwright.
| Tool | Purpose | Requires @axe-core/playwright | |------|---------|------------------------------| | `run_playwright_keyboard_scan` | Tab-order traversal, keyboard trap detection | No | | `run_playwright_state_scan` | Click triggers, scan revealed content with axe-core | Yes | | `run_playwright_viewport_scan` | Multi-viewport axe-core + touch target measurement | Yes | | `run_playwright_contrast_scan` | Computed-style contrast ratio after CSS cascade | No | | `run_playwright_a11y_tree` | Browser accessibility tree snapshot | No |
import AxeBuilder from '@axe-core/playwright';
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
.analyze();const results = await new AxeBuilder({ page })
.include('.modal-content')
.withTags(['wcag2a', 'wcag2aa'])
.analyze();const results = await new AxeBuilder({ page })
.include('#hero-image')
.withRules(['image-alt'])
.analyze();
expect(results.violations).toEqual([]);await page.click('[aria-expanded="false"]');
await page.waitForSelector('.accordion-content', { state: 'visible' });
const results = await new AxeBuilder({ page })
.include('.accordion-content')
.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.click('[data-modal-trigger]');
await page.waitForSelector('[role="dialog"]', { 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.click('[data-open-modal]');
await page.waitForSelector('[role="dialog"]', { state: 'visible' });
// 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@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Start dev server
run: npm run dev &
env:
CI: true
- name: Wait for server
run: npx wait-on http://localhost:3000 --timeout 30000
- name: Run accessibility tests
run: npx playwright test tests/a11y/
- uses: actions/upload-artifact@v4
if: failure()
with:
name: a11y-test-results
path: test-results/// playwright.config.js (a11y section)
export default {
testDir: './tests/a11y',
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
// Use Chromium only — @axe-core/playwright is Chromium-validated
browserName: 'chromium',
},
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
};let _playwrightAvailable = null; as
WCAG 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 on its own.
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 merges their findings.
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.