WCAG 2.1 AA/AAA accessibility compliance specialist for ARIA attributes, keyboard navigation, screen reader testing, color contrast validation, semantic HTML, and automated a11y testing with axe-core/Lighthouse. Use when ensuring web accessibility, meeting legal compliance (ADA, Section 508), implementing inclusive design, or requiring WCAG best practices. Handles focus management, live regions, accessible forms, and assistive technology compatibility.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
name: wcag-accessibility description: WCAG 2.1 AA/AAA accessibility compliance specialist for ARIA attributes, keyboard navigation, screen reader testing, color contrast validation, semantic HTML, and automated a11y testing with axe-core/Lighthouse. Use when ensuring web accessibility, meeting legal compliance (ADA, Section 508), implementing inclusive design, or requiring WCAG best practices. Handles focus management, live regions, accessible forms, and assistive technology compatibility. category: Compliance complexity: Medium triggers:
Use this skill when conducting compliance audits, implementing regulatory controls, preparing for certification audits, validating GDPR/HIPAA/SOC2/PCI-DSS/ISO27001 adherence, or documenting security and privacy practices for regulated industries.
Do NOT use for non-regulated applications, internal tools without compliance requirements, proof-of-concept projects, or general security audits (use security-analyzer instead). Avoid using for unauthorized compliance testing of third-party systems.
All compliance findings MUST be validated through:
Expert web accessibility implementation for WCAG 2.1 AA/AAA compliance and inclusive design.
Comprehensive accessibility expertise including ARIA attributes, keyboard navigation, screen reader compatibility, color contrast, semantic HTML, and automated testing. Ensures web applications are usable by people with disabilities and meet legal requirements.
Required: HTML/CSS, JavaScript basics, understanding of semantic HTML
Agents: tester, reviewer, code-analyzer, coder
Step 1: Semantic HTML with Labels
<!-- ✅ GOOD: Proper labels and structure -->
<form>
<div class="form-group">
<label for="email">Email Address *</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
aria-describedby="email-help email-error"
/>
<small id="email-help">We'll never share your email.</small>
<span id="email-error" role="alert" aria-live="polite"></span>
</div>
<fieldset>
<legend>Choose your plan</legend>
<div>
<input type="radio" id="plan-free" name="plan" value="free" />
<label for="plan-free">Free</label>
</div>
<div>
<input type="radio" id="plan-pro" name="plan" value="pro" />
<label for="plan-pro">Pro</label>
</div>
</fieldset>
<button type="submit">Subscribe</button>
</form>
<!-- ❌ BAD: No labels, placeholder only -->
<form>
<input type="email" placeholder="Email" />
<input type="text" placeholder="Plan" />
<button>Submit</button>
</form>
Step 2: Client-Side Validation with Accessibility
const form = document.querySelector('form');
const emailInput = document.getElementById('email');
const emailError = document.getElementById('email-error');
emailInput.addEventListener('blur', () => {
if (!emailInput.validity.valid) {
emailError.textContent = 'Please enter a valid email address.';
emailInput.setAttribute('aria-invalid', 'true');
} else {
emailError.textContent = '';
emailInput.removeAttribute('aria-invalid');
}
});
Step 1: Focus Management
// Modal with focus trap
class AccessibleModal {
constructor(modalElement) {
this.modal = modalElement;
this.focusableElements = this.modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
this.firstFocusable = this.focusableElements[0];
this.lastFocusable = this.focusableElements[this.focusableElements.length - 1];
}
open() {
this.previouslyFocused = document.activeElement;
this.modal.setAttribute('aria-hidden', 'false');
this.modal.addEventListener('keydown', this.trapFocus.bind(this));
this.firstFocusable.focus();
}
close() {
this.modal.setAttribute('aria-hidden', 'true');
this.modal.removeEventListener('keydown', this.trapFocus);
this.previouslyFocused.focus();
}
trapFocus(e) {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === this.firstFocusable) {
e.preventDefault();
this.lastFocusable.focus();
}
} else {
if (document.activeElement === this.lastFocusable) {
e.preventDefault();
this.firstFocusable.focus();
}
}
}
if (e.key === 'Escape') {
this.close();
}
}
}
Step 2: Skip Links
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav><!-- navigation --></nav>
<main id="main-content" tabindex="-1">
<!-- main content -->
</main>
</body>
<style>
.skip-link {
position: absolute;
left: -9999px;
z-index: 999;
padding: 1em;
background: #000;
color: #fff;
}
.skip-link:focus {
left: 50%;
transform: translateX(-50%);
top: 0;
}
</style>
Step 1: Accordion Component
<div class="accordion">
<h3>
<button
aria-expanded="false"
aria-controls="section1"
id="accordion1"
>
<span>Section 1</span>
<span aria-hidden="true">+</span>
</button>
</h3>
<div id="section1" role="region" aria-labelledby="accordion1" hidden>
<p>Section 1 content</p>
</div>
<h3>
<button
aria-expanded="false"
aria-controls="section2"
id="accordion2"
>
<span>Section 2</span>
<span aria-hidden="true">+</span>
</button>
</h3>
<div id="section2" role="region" aria-labelledby="accordion2" hidden>
<p>Section 2 content</p>
</div>
</div>
<script>
document.querySelectorAll('.accordion button').forEach(button => {
button.addEventListener('click', () => {
const expanded = button.getAttribute('aria-expanded') === 'true';
const content = document.getElementById(button.getAttribute('aria-controls'));
button.setAttribute('aria-expanded', !expanded);
content.hidden = expanded;
button.querySelector('[aria-hidden]').textContent = expanded ? '+' : '-';
});
});
</script>
Step 2: Live Regions for Dynamic Content
<div aria-live="polite" aria-atomic="true" class="sr-only">
<!-- Announces updates to screen readers -->
</div>
<script>
function announceToScreenReader(message) {
const liveRegion = document.querySelector('[aria-live]');
liveRegion.textContent = message;
setTimeout(() => {
liveRegion.textContent = '';
}, 1000);
}
// Usage
announceToScreenReader('Item added to cart');
</script>
Step 1: WCAG AA Contrast Requirements
/* ✅ GOOD: 4.5:1 contrast for normal text (WCAG AA) */
.text {
color: #595959; /* on white background */
background: #ffffff;
}
/* ✅ GOOD: 3:1 contrast for large text (18pt+ or 14pt+ bold) */
.large-text {
font-size: 18pt;
color: #767676;
background: #ffffff;
}
/* ❌ BAD: 2.5:1 contrast (fails WCAG AA) */
.low-contrast {
color: #a8a8a8;
background: #ffffff;
}
Step 2: Focus Indicators
/* ✅ GOOD: Visible focus indicator */
button:focus,
a:focus {
outline: 3px solid #4A90E2;
outline-offset: 2px;
}
/* Enhanced focus for keyboard users only */
button:focus-visible {
outline: 3px solid #4A90E2;
outline-offset: 2px;
}
/* ❌ BAD: Removing outline without replacement */
button:focus {
outline: none; /* Don't do this */
}
Step 3: Responsive Text and Zoom
/* ✅ GOOD: Relative units for zoom support */
body {
font-size: 16px; /* Base size */
}
h1 {
font-size: 2rem; /* 32px, scales with zoom */
}
/* ✅ GOOD: Allow text to reflow at 320px viewport */
@media (max-width: 320px) {
.container {
width: 100%;
max-width: none;
}
}
Step 1: axe-core Integration
npm install --save-dev @axe-core/playwright
// tests/accessibility.test.js
import { test, expect } from '@playwright/test';
import { injectAxe, checkA11y } from '@axe-core/playwright';
test('homepage should be accessible', async ({ page }) => {
await page.goto('http://localhost:3000');
await injectAxe(page);
await checkA11y(page, null, {
detailedReport: true,
detailedReportOptions: {
html: true,
},
});
});
test('forms should be accessible', async ({ page }) => {
await page.goto('http://localhost:3000/form');
await injectAxe(page);
await checkA11y(page, '#form-container', {
axeOptions: {
runOnly: {
type: 'tag',
values: ['wcag2a', 'wcag2aa', 'wcag21aa'],
},
},
});
});
Step 2: Lighthouse CI
# .github/workflows/a11y.yml
name: Accessibility Audit
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v9
with:
urls: |
http://localhost:3000
configPath: ./lighthouserc.json
// lighthouserc.json
{
"ci": {
"assert": {
"assertions": {
"categories:accessibility": ["error", { "minScore": 0.9 }]
}
}
}
}
1. Semantic HTML First
<!-- ✅ GOOD -->
<nav><ul><li><a href="/">Home</a></li></ul></nav>
<main><article><h1>Title</h1></article></main>
<footer>Footer content</footer>
<!-- ❌ BAD -->
<div class="nav"><div class="link">Home</div></div>
<div class="content"><div class="title">Title</div></div>
2. Alt Text for Images
<!-- ✅ GOOD: Descriptive alt text -->
<img src="chart.png" alt="Bar chart showing sales increased 40% in Q4" />
<!-- ✅ GOOD: Decorative image -->
<img src="decorative.png" alt="" role="presentation" />
<!-- ❌ BAD: Missing alt or generic text -->
<img src="chart.png" alt="image" />
3. Headings Hierarchy
<!-- ✅ GOOD: Logical heading order -->
<h1>Page Title</h1>
<h2>Section 1</h2>
<h3>Subsection 1.1</h3>
<h2>Section 2</h2>
<!-- ❌ BAD: Skipping levels -->
<h1>Page Title</h1>
<h4>Section</h4>
4. Links vs Buttons
<!-- ✅ GOOD: Link for navigation -->
<a href="/page">Go to page</a>
<!-- ✅ GOOD: Button for actions -->
<button type="button" onclick="showModal()">Open Modal</button>
<!-- ❌ BAD: Link styled as button for action -->
<a href="#" onclick="showModal()">Open Modal</a>
5. Tables for Tabular Data
<table>
<caption>Monthly Sales Report</caption>
<thead>
<tr>
<th scope="col">Month</th>
<th scope="col">Sales</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">January</th>
<td>$10,000</td>
</tr>
</tbody>
</table>
Keyboard Navigation:
Screen Reader:
Visual:
Issue: Screen reader skipping content
Solution: Check if content is visually hidden incorrectly (use .sr-only class, not display: none)
Issue: Tab order incorrect
Solution: Avoid positive tabindex values, ensure DOM order matches visual order
Issue: Form errors not announced
Solution: Use aria-live="polite" or role="alert" for error messages
react-specialist: Accessible React componentstesting-quality: Automated accessibility testingstyle-audit: CSS code quality and a11ymcp__playwright__browser_snapshot for visual regression testingmcp__playwright__browser_evaluate for running axe-coremcp__memory-mcp__memory_store for a11y patternsSkill Version: 1.0.0 Last Updated: 2025-11-02