Accessibility testing with axe-core and Playwright. Use when checking WCAG compliance, finding a11y issues, ensuring keyboard navigation, or testing screen reader compatibility.
66
80%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./skills/accessibility-testing/SKILL.mdAutomated accessibility testing for WCAG 2.1 AA/AAA compliance using axe-core integrated with Playwright.
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage has no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});npm install -D @axe-core/playwrightimport { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('check entire page', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});test('check navigation accessibility', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.include('nav')
.analyze();
expect(results.violations).toEqual([]);
});test('check page excluding ads', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.exclude('.advertisement')
.exclude('#third-party-widget')
.analyze();
expect(results.violations).toEqual([]);
});test('WCAG 2.1 Level A compliance', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag21a'])
.analyze();
expect(results.violations).toEqual([]);
});test('WCAG 2.1 Level AA compliance', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(results.violations).toEqual([]);
});test('WCAG 2.1 Level AAA compliance', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag2aaa', 'wcag21a', 'wcag21aa', 'wcag21aaa'])
.analyze();
expect(results.violations).toEqual([]);
});test('accessibility best practices', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['best-practice'])
.analyze();
expect(results.violations).toEqual([]);
});test('check specific rules', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withRules(['color-contrast', 'image-alt', 'label', 'link-name'])
.analyze();
expect(results.violations).toEqual([]);
});test('check except known issues', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.disableRules(['color-contrast']) // Known issue, tracked separately
.analyze();
expect(results.violations).toEqual([]);
});test('verify tab order', async ({ page }) => {
await page.goto('/');
const expectedOrder = ['#search', '#nav-home', '#nav-about', '#nav-contact', '#main-content'];
for (const selector of expectedOrder) {
await page.keyboard.press('Tab');
const focused = await page.evaluate(() => document.activeElement?.id || document.activeElement?.className);
expect(`#${focused}`).toBe(selector);
}
});test('focus indicators are visible', async ({ page }) => {
await page.goto('/');
await page.keyboard.press('Tab');
const focusedElement = page.locator(':focus');
const outline = await focusedElement.evaluate(el => {
const styles = window.getComputedStyle(el);
return styles.outline || styles.boxShadow;
});
expect(outline).not.toBe('none');
});test('skip link works', async ({ page }) => {
await page.goto('/');
// First tab should focus skip link
await page.keyboard.press('Tab');
await expect(page.locator(':focus')).toHaveText(/skip to/i);
// Enter should jump to main content
await page.keyboard.press('Enter');
await expect(page.locator(':focus')).toHaveAttribute('id', 'main-content');
});test('color contrast meets WCAG AA', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withRules(['color-contrast'])
.analyze();
if (results.violations.length > 0) {
console.log('Contrast violations:');
results.violations[0].nodes.forEach(node => {
console.log(` - ${node.html}`);
console.log(` ${node.failureSummary}`);
});
}
expect(results.violations).toEqual([]);
});test('form is accessible', async ({ page }) => {
await page.goto('/contact');
// Check labels
const inputs = page.locator('input:not([type="hidden"])');
const count = await inputs.count();
for (let i = 0; i < count; i++) {
const input = inputs.nth(i);
const id = await input.getAttribute('id');
const ariaLabel = await input.getAttribute('aria-label');
const ariaLabelledBy = await input.getAttribute('aria-labelledby');
const label = page.locator(`label[for="${id}"]`);
const hasLabel = await label.count() > 0 || ariaLabel || ariaLabelledBy;
expect(hasLabel).toBeTruthy();
}
// Run axe on form
const results = await new AxeBuilder({ page })
.include('form')
.analyze();
expect(results.violations).toEqual([]);
});test('all images have alt text', async ({ page }) => {
await page.goto('/');
const images = page.locator('img');
const count = await images.count();
for (let i = 0; i < count; i++) {
const img = images.nth(i);
const alt = await img.getAttribute('alt');
const role = await img.getAttribute('role');
// Images must have alt OR be decorative (role="presentation")
const isAccessible = alt !== null || role === 'presentation' || role === 'none';
expect(isAccessible).toBeTruthy();
}
});test('ARIA attributes are valid', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['cat.aria'])
.analyze();
expect(results.violations).toEqual([]);
});import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('accessibility audit', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
// Generate detailed report
if (results.violations.length > 0) {
console.log('\n=== Accessibility Violations ===\n');
results.violations.forEach(violation => {
console.log(`Rule: ${violation.id}`);
console.log(`Impact: ${violation.impact}`);
console.log(`Description: ${violation.description}`);
console.log(`Help: ${violation.helpUrl}`);
console.log(`Affected elements:`);
violation.nodes.forEach(node => {
console.log(` - ${node.html}`);
console.log(` ${node.failureSummary}`);
});
console.log('');
});
}
expect(results.violations).toEqual([]);
});import fs from 'fs';
test('save accessibility report', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
// Save JSON report
fs.writeFileSync(
'accessibility-report.json',
JSON.stringify(results, null, 2)
);
// Save HTML report
const htmlReport = generateHtmlReport(results);
fs.writeFileSync('accessibility-report.html', htmlReport);
});
function generateHtmlReport(results: any): string {
return `
<!DOCTYPE html>
<html>
<head><title>Accessibility Report</title></head>
<body>
<h1>Accessibility Report</h1>
<p>Violations: ${results.violations.length}</p>
<p>Passes: ${results.passes.length}</p>
${results.violations.map(v => `
<div style="border:1px solid red;padding:10px;margin:10px 0">
<h3>${v.id}</h3>
<p><strong>Impact:</strong> ${v.impact}</p>
<p>${v.description}</p>
<p><a href="${v.helpUrl}">More info</a></p>
</div>
`).join('')}
</body>
</html>
`;
}- name: Run accessibility tests
run: npx playwright test --grep @a11y
- name: Upload a11y report
if: failure()
uses: actions/upload-artifact@v4
with:
name: accessibility-report
path: accessibility-report.htmlreferences/wcag-checklist.md - WCAG 2.1 compliance checklistreferences/common-issues.md - Most common a11y issues and fixes93ed392
Also appears in
since Sep 12, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.