一键导入
accessibility-checker
Validate WCAG 2.1 Level AA compliance and accessibility best practices. Use when performing accessibility audits and WCAG certification.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Validate WCAG 2.1 Level AA compliance and accessibility best practices. Use when performing accessibility audits and WCAG certification.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Analyze feature requirements, dependencies, and security considerations. Use when starting feature implementation from GitHub issues to understand scope, technical feasibility, and risks.
Design system architecture, API contracts, and data flows. Use when translating analyzed requirements into technical design for feature implementation.
Implement features with code, tests, and documentation. Use when building features from approved designs following TDD and project coding standards.
Validate code quality, test coverage, performance, and security. Use when verifying implemented features meet all standards and requirements before marking complete.
Analyze feature requirements, dependencies, and security considerations. Use when starting feature implementation from GitHub issues to understand scope, technical feasibility, and risks.
TODO: Brief description of what the Skill does and when to use it
| 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 |
This skill provides comprehensive accessibility validation against WCAG 2.1 Level AA standards, combining automated testing with manual verification procedures.
Using Axe-core with Playwright:
// Install axe-core
npm install -D @axe-core/playwright
// Accessibility test
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:
# Create script to 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
1.1 Text Alternatives:
Check Images:
# Find images without alt text
grep -r "<img" src/ | grep -v "alt="
# Using Playwright
await page.locator('img:not([alt])').count(); // Should be 0
Checklist:
1.3 Adaptable:
// Test: Content order makes sense
test('content order is logical', async ({ page }) => {
await page.goto('/');
// Disable CSS to check content order
await page.addStyleTag({ content: '* { all: unset !important; }' });
const textContent = await page.textContent('body');
// Verify content reads logically
});
// Test: Responsive tables
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);
// Check for headers
await expect(table.locator('th')).toHaveCount(greaterThan(0));
// Check for scope attributes
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:
# Manual check with browser DevTools or:
# Use axe-core for automated checking
# Check specific contrast ratios
# Text: 4.5:1 minimum
# Large text (18pt+): 3:1 minimum
# UI components: 3:1 minimum
// Test: 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
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');
// Check focus is visible
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
2.1 Keyboard Accessible:
// Test: Full keyboard navigation
test('all functionality available via keyboard', async ({ page }) => {
await page.goto('/');
// Tab through all interactive elements
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);
// Verify element is focusable
expect(['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA']).toContain(tagName);
}
// Verify no keyboard trap
// Tab through all elements without getting stuck
});
// Test: Skip links
test('skip link allows bypassing navigation', async ({ page }) => {
await page.goto('/');
// Press Tab to focus skip link
await page.keyboard.press('Tab');
const skipLink = page.locator('a[href="#main-content"]');
await expect(skipLink).toBeFocused();
// Activate skip link
await page.keyboard.press('Enter');
// Verify focus moved to main content
const mainContent = page.locator('#main-content');
await expect(mainContent).toBeFocused();
});
Checklist:
2.4 Navigable:
// Test: Page title
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 structure
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])))
);
// Check h1 exists and is unique
const h1Count = levels.filter(l => l === 1).length;
expect(h1Count).toBe(1);
// Check no skipped levels
for (let i = 1; i < levels.length; i++) {
const diff = levels[i] - levels[i-1];
expect(diff).toBeLessThanOrEqual(1);
}
});
// Test: Link purpose
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);
// Avoid generic text
if (text) {
expect(['click here', 'read more', 'link']).not.toContain(text.toLowerCase().trim());
}
}
});
Checklist:
2.5 Input Modalities:
// Test: Touch target size
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
3.1 Readable:
# Check language attribute
grep -r "<html" src/ | grep -v 'lang='
# Playwright check
await expect(page.locator('html')).toHaveAttribute('lang');
Checklist:
3.2 Predictable:
// Test: Consistent navigation
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);
}
// Verify all pages have same navigation
expect(navStructures[0]).toEqual(navStructures[1]);
expect(navStructures[0]).toEqual(navStructures[2]);
});
// Test: No unexpected context changes
test('focus does not trigger unexpected changes', async ({ page }) => {
await page.goto('/form');
const url = page.url();
// Tab through form
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
// URL should not change on focus
expect(page.url()).toBe(url);
});
Checklist:
3.3 Input Assistance:
// Test: Form labels
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: Error identification
test('errors are clearly identified', async ({ page }) => {
await page.goto('/form');
// Submit empty form
await page.click('button[type="submit"]');
// Check for error messages
const errors = page.locator('[role="alert"], .error-message');
await expect(errors).toHaveCount(greaterThan(0));
// Errors should be associated with fields
const inputs = await page.locator('input[aria-invalid="true"]').all();
expect(inputs.length).toBeGreaterThan(0);
});
Checklist:
Deliverable: Understandable compliance report
4.1 Compatible:
# Validate HTML
npx html-validate "src/**/*.html"
# Check ARIA usage
grep -r "aria-" src/ --include="*.html" --include="*.jsx" --include="*.tsx"
// Test: Valid ARIA
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: Name, Role, Value
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
VoiceOver (macOS):
# Enable VoiceOver: Cmd+F5
# Navigate: VO+arrows
# Interact: VO+Shift+Down
# Stop interacting: VO+Shift+Up
NVDA (Windows - Free):
# Download: https://www.nvaccess.org/
# Navigate: Arrow keys
# Read all: Insert+Down
# Elements list: Insert+F7
Manual Checklist:
Manual Test Script:
# Browser zoom to 200%
# Verify:
# - All content visible
# - No horizontal scrolling
# - Text readable
# - Functionality works
# - Touch targets remain usable
# 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]
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.