| name | accessibility-checker |
| description | Validate WCAG 2.1 Level AA compliance and accessibility best practices. Use when performing accessibility audits and WCAG certification. |
| allowed-tools | Read, Grep, Glob, Bash, mcp__playwright-mcp__playwright_navigate, mcp__playwright-mcp__playwright_screenshot, mcp__playwright-mcp__playwright_evaluate |
Accessibility Checker Skill
Purpose
This skill provides comprehensive accessibility validation against WCAG 2.1 Level AA standards, combining automated testing with manual verification procedures.
When to Use
- Accessibility audits for new features
- WCAG 2.1 Level AA compliance checks
- Pre-release accessibility validation
- Accessibility regression testing
- Legal compliance verification (ADA, Section 508)
WCAG 2.1 Level AA Validation Workflow
1. Automated Accessibility Scanning
Using Axe-core with Playwright:
npm install -D @axe-core/playwright
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('page should not have accessibility violations', async ({ page }) => {
await page.goto('/');
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
Scan All Pages:
cat > scripts/accessibility-scan.js << 'EOF'
const { chromium } = require('playwright');
const AxeBuilder = require('@axe-core/playwright').default;
async function scanPage(url) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(url);
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
await browser.close();
return results;
}
// Scan multiple pages
const pages = [
'http://localhost:3000/',
'http://localhost:3000/about',
'http://localhost:3000/products',
];
(async () => {
for (const url of pages) {
console.log(`Scanning ${url}`);
const results = await scanPage(url);
console.log(`Violations: ${results.violations.length}`);
}
})();
EOF
node scripts/accessibility-scan.js
Deliverable: Automated scan results with violation list
2. WCAG 2.1 Principle: Perceivable
1.1 Text Alternatives:
Check Images:
grep -r "<img" src/ | grep -v "alt="
await page.locator('img:not([alt])').count(); // Should be 0
Checklist:
1.3 Adaptable:
test('content order is logical', async ({ page }) => {
await page.goto('/');
await page.addStyleTag({ content: '* { all: unset !important; }' });
const textContent = await page.textContent('body');
});
test('tables are responsive', async ({ page }) => {
await page.goto('/data');
const tables = page.locator('table');
const count = await tables.count();
for (let i = 0; i < count; i++) {
const table = tables.nth(i);
await expect(table.locator('th')).toHaveCount(greaterThan(0));
const headers = await table.locator('th').all();
for (const header of headers) {
const scope = await header.getAttribute('scope');
expect(['col', 'row', 'colgroup', 'rowgroup']).toContain(scope);
}
}
});
Checklist:
1.4 Distinguishable:
Color Contrast:
test('text has sufficient color contrast', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['color-contrast'])
.analyze();
expect(results.violations).toEqual([]);
});
test('focus indicators are visible', async ({ page }) => {
await page.goto('/');
const links = page.locator('a, button, input');
const count = await links.count();
for (let i = 0; i < count; i++) {
await page.keyboard.press('Tab');
const focused = await page.evaluateHandle(() => document.activeElement);
const outline = await focused.evaluate(el =>
window.getComputedStyle(el).outline
);
expect(outline).not.toBe('none');
}
});
Checklist:
Deliverable: Perceivable compliance report
3. WCAG 2.1 Principle: Operable
2.1 Keyboard Accessible:
test('all functionality available via keyboard', async ({ page }) => {
await page.goto('/');
const interactiveElements = await page.locator(
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
).all();
for (let i = 0; i < interactiveElements.length; i++) {
await page.keyboard.press('Tab');
const focused = await page.evaluateHandle(() => document.activeElement);
const tagName = await focused.evaluate(el => el.tagName);
expect(['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA']).toContain(tagName);
}
});
test('skip link allows bypassing navigation', async ({ page }) => {
await page.goto('/');
await page.keyboard.press('Tab');
const skipLink = page.locator('a[href="#main-content"]');
await expect(skipLink).toBeFocused();
await page.keyboard.press('Enter');
const mainContent = page.locator('#main-content');
await expect(mainContent).toBeFocused();
});
Checklist:
2.4 Navigable:
test('pages have descriptive titles', async ({ page }) => {
await page.goto('/products');
const title = await page.title();
expect(title).toContain('Products');
expect(title.length).toBeGreaterThan(5);
});
test('heading hierarchy is logical', async ({ page }) => {
await page.goto('/');
const headings = await page.locator('h1, h2, h3, h4, h5, h6').all();
const levels = await Promise.all(
headings.map(h => h.evaluate(el => parseInt(el.tagName[1])))
);
const h1Count = levels.filter(l => l === 1).length;
expect(h1Count).toBe(1);
for (let i = 1; i < levels.length; i++) {
const diff = levels[i] - levels[i-1];
expect(diff).toBeLessThanOrEqual(1);
}
});
test('links have descriptive text', async ({ page }) => {
await page.goto('/');
const links = await page.locator('a').all();
for (const link of links) {
const text = await link.textContent();
const ariaLabel = await link.getAttribute('aria-label');
const title = await link.getAttribute('title');
const hasText = text && text.trim().length > 0;
const hasLabel = ariaLabel && ariaLabel.length > 0;
const hasTitle = title && title.length > 0;
expect(hasText || hasLabel || hasTitle).toBe(true);
if (text) {
expect(['click here', 'read more', 'link']).not.toContain(text.toLowerCase().trim());
}
}
});
Checklist:
2.5 Input Modalities:
test('touch targets are at least 44x44 pixels', async ({ page }) => {
await page.goto('/');
const targets = await page.locator('a, button, input, [role="button"]').all();
for (const target of targets) {
const box = await target.boundingBox();
if (box) {
expect(box.width).toBeGreaterThanOrEqual(44);
expect(box.height).toBeGreaterThanOrEqual(44);
}
}
});
Checklist:
Deliverable: Operable compliance report
4. WCAG 2.1 Principle: Understandable
3.1 Readable:
grep -r "<html" src/ | grep -v 'lang='
await expect(page.locator('html')).toHaveAttribute('lang');
Checklist:
3.2 Predictable:
test('navigation is consistent across pages', async ({ page }) => {
const pages = ['/', '/about', '/products'];
const navStructures = [];
for (const url of pages) {
await page.goto(url);
const navItems = await page.locator('nav a').allTextContents();
navStructures.push(navItems);
}
expect(navStructures[0]).toEqual(navStructures[1]);
expect(navStructures[0]).toEqual(navStructures[2]);
});
test('focus does not trigger unexpected changes', async ({ page }) => {
await page.goto('/form');
const url = page.url();
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
expect(page.url()).toBe(url);
});
Checklist:
3.3 Input Assistance:
test('all form inputs have labels', async ({ page }) => {
await page.goto('/form');
const inputs = await page.locator('input, select, textarea').all();
for (const input of inputs) {
const id = await input.getAttribute('id');
const ariaLabel = await input.getAttribute('aria-label');
const ariaLabelledby = await input.getAttribute('aria-labelledby');
if (id) {
const label = page.locator(`label[for="${id}"]`);
const hasLabel = await label.count() > 0;
expect(hasLabel || ariaLabel || ariaLabelledby).toBe(true);
}
}
});
test('errors are clearly identified', async ({ page }) => {
await page.goto('/form');
await page.click('button[type="submit"]');
const errors = page.locator('[role="alert"], .error-message');
await expect(errors).toHaveCount(greaterThan(0));
const inputs = await page.locator('input[aria-invalid="true"]').all();
expect(inputs.length).toBeGreaterThan(0);
});
Checklist:
Deliverable: Understandable compliance report
5. WCAG 2.1 Principle: Robust
4.1 Compatible:
npx html-validate "src/**/*.html"
grep -r "aria-" src/ --include="*.html" --include="*.jsx" --include="*.tsx"
test('ARIA attributes are valid', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['cat.aria'])
.analyze();
expect(results.violations).toEqual([]);
});
test('UI components have accessible name and role', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag412'])
.analyze();
expect(results.violations).toEqual([]);
});
Checklist:
Deliverable: Robust compliance report
Manual Testing Procedures
Screen Reader Testing
VoiceOver (macOS):
NVDA (Windows - Free):
Manual Checklist:
Keyboard Testing
Manual Test Script:
- Unplug mouse
- Tab through entire page
- Verify all functionality accessible
- Verify focus always visible
- Test with screen reader
- Test keyboard shortcuts
- Verify no keyboard traps
Zoom and Reflow Testing
Accessibility Report Format
# WCAG 2.1 Level AA Accessibility Report
**Date**: [YYYY-MM-DD]
**Application**: [name]
**Pages Tested**: [count]
**Testing Method**: Automated + Manual
## Executive Summary
**Overall Compliance**: [XX]% compliant
- **Critical Issues**: [count] (must fix)
- **Serious Issues**: [count] (should fix)
- **Moderate Issues**: [count] (nice to fix)
- **Minor Issues**: [count] (best practice)
## WCAG 2.1 Compliance Status
| Principle | Level A | Level AA | Notes |
|-----------|---------|----------|-------|
| Perceivable | ✅/❌ ([X]/[Y]) | ✅/❌ ([X]/[Y]) | [summary] |
| Operable | ✅/❌ ([X]/[Y]) | ✅/❌ ([X]/[Y]) | [summary] |
| Understandable | ✅/❌ ([X]/[Y]) | ✅/❌ ([X]/[Y]) | [summary] |
| Robust | ✅/❌ ([X]/[Y]) | ✅/❌ ([X]/[Y]) | [summary] |
## Detailed Findings
### Critical: [Issue Title]
**WCAG Criterion**: [X.X.X Title]
**Level**: A/AA
**Impact**: [who is affected]
**Pages**: [list of pages]
**Issue**: [description]
**User Impact**: [how it affects users]
**How to Fix**:
```html
<!-- Before -->
<img src="logo.png">
<!-- After -->
<img src="logo.png" alt="Company Logo">
WCAG Reference: [link]
Testing Summary
Automated Testing (Axe-core)
- Pages scanned: [count]
- Violations found: [count]
- Rules checked: [count]
Manual Testing
- Keyboard navigation: ✅/❌
- Screen reader (NVDA): ✅/❌
- Screen reader (VoiceOver): ✅/❌
- Zoom to 200%: ✅/❌
- Mobile accessibility: ✅/❌
Browser Testing
- Chrome: ✅/❌
- Firefox: ✅/❌
- Safari: ✅/❌
- Edge: ✅/❌
Recommendations
Immediate (Critical)
- [Fix 1]
- [Fix 2]
Short-term (Serious)
- [Fix 1]
Long-term (Moderate)
- [Fix 1]
Resources
Certification
This application [IS / IS NOT] compliant with WCAG 2.1 Level AA.
Assessor: [name]
Date: [YYYY-MM-DD]
Next Review: [YYYY-MM-DD]
---
## Best Practices
**Testing Approach:**
- Combine automated and manual testing
- Test with actual assistive technologies
- Include users with disabilities in testing
- Test on multiple devices and browsers
**Common Issues:**
- Missing alt text on images
- Insufficient color contrast
- Missing form labels
- Keyboard traps
- Poor heading structure
- Missing ARIA labels
- Non-semantic HTML
**Quick Wins:**
- Add alt attributes to images
- Increase color contrast
- Add skip links
- Use semantic HTML
- Add form labels
- Logical heading hierarchy
---
## Remember
- **30% rule**: Automated tools catch ~30% of issues, manual testing needed
- **Real users**: Test with people who use assistive technologies
- **Progressive enhancement**: Build accessibility in, don't bolt it on
- **Keyboard first**: If it works with keyboard, it works with most AT
- **Semantic HTML**: Use proper elements (button, not div)
- **ARIA last resort**: Use semantic HTML first, ARIA when needed
- **Test early**: Accessibility issues are cheaper to fix early
- **Continuous**: Accessibility is ongoing, not one-time
Your goal is to ensure digital experiences are accessible to all users, regardless of ability or assistive technology used.