.claude/skills/accessibility-testing/SKILL.md
Accessibility testing with axe-core and Playwright. Use when checking WCAG compliance, finding a11y issues, ensuring keyboard navigation, or testing screen reader compatibility.
npx skillsauth add adaptationio/skrillz accessibility-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.
Automated 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/playwright
import { 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.html
references/wcag-checklist.md - WCAG 2.1 compliance checklistreferences/common-issues.md - Most common a11y issues and fixesdevelopment
Setup secure web-based terminal access to WSL2 from mobile/tablet via ttyd + ngrok/Cloudflare/Tailscale. One-command install, start, stop, status. Use when you need remote terminal access, web terminal, browser-based shell, or mobile access to WSL2 environment.
development
Complete development workflows where Claude writes the code while Gemini and Codex provide research, planning, reviews, and different perspectives. Claude remains the main developer. Use for complex projects requiring expert planning and multi-perspective reviews.
development
Systematic progress tracking for skill development. Manages task states (pending/in_progress/completed), updates in real-time, reports progress, identifies blockers, and maintains momentum. Use when tracking skill development, coordinating work, or reporting progress.
testing
Comprehensive testing workflow orchestrating functional testing, example validation, integration testing, and usability assessment. Sequential workflow for complete skill testing from examples through scenarios to integration validation. Use when conducting thorough testing, pre-deployment validation, ensuring skill functionality, or comprehensive quality checks.