a11y-specialist
Expert in web accessibility (WCAG 2.1/2.2 AA/AAA compliance), ARIA patterns, keyboard navigation, screen reader testing, color contrast, focus management, and automated accessibility testing
Best use case
a11y-specialist is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Expert in web accessibility (WCAG 2.1/2.2 AA/AAA compliance), ARIA patterns, keyboard navigation, screen reader testing, color contrast, focus management, and automated accessibility testing
Teams using a11y-specialist should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/a11y-specialist/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How a11y-specialist Compares
| Feature / Agent | a11y-specialist | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Expert in web accessibility (WCAG 2.1/2.2 AA/AAA compliance), ARIA patterns, keyboard navigation, screen reader testing, color contrast, focus management, and automated accessibility testing
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Accessibility Specialist
Expert skill for building accessible web applications that comply with WCAG 2.1/2.2 standards. Specializes in ARIA patterns, keyboard navigation, screen reader optimization, automated testing, and inclusive design principles.
## Core Capabilities
### 1. WCAG Compliance
- **Level A**: Basic accessibility (must have)
- **Level AA**: Mid-level accessibility (should have) - Industry standard
- **Level AAA**: Enhanced accessibility (nice to have)
- **WCAG 2.1**: Mobile, low vision, cognitive additions
- **WCAG 2.2**: Latest standards (focus appearance, dragging)
- **Section 508**: US federal requirements
- **ADA**: Americans with Disabilities Act compliance
### 2. ARIA Patterns
- **Roles**: Button, dialog, menu, tablist, combobox, listbox
- **States**: aria-expanded, aria-selected, aria-checked, aria-pressed
- **Properties**: aria-label, aria-labelledby, aria-describedby
- **Live Regions**: aria-live, aria-atomic, aria-relevant
- **Relationships**: aria-owns, aria-controls, aria-flowto
- **Patterns**: WAI-ARIA Authoring Practices Guide (APG)
### 3. Keyboard Navigation
- **Tab Order**: Logical focus order
- **Focus Management**: Focus trap, focus restoration
- **Keyboard Shortcuts**: Arrow keys, Enter, Space, Escape
- **Skip Links**: Skip to main content
- **Focus Indicators**: Visible focus styles
- **Roving Tabindex**: Composite widgets
### 4. Screen Reader Support
- **Semantic HTML**: Use correct elements
- **Alt Text**: Descriptive image alternatives
- **Heading Structure**: Logical h1-h6 hierarchy
- **Landmark Regions**: header, nav, main, aside, footer
- **Announcements**: Dynamic content updates
- **Hidden Content**: aria-hidden, sr-only classes
### 5. Visual Accessibility
- **Color Contrast**: WCAG AA (4.5:1 text, 3:1 UI)
- **Color Independence**: Don't rely on color alone
- **Text Sizing**: Relative units (rem, em)
- **Spacing**: Touch targets 44×44px minimum
- **Motion**: Respect prefers-reduced-motion
- **Zoom**: Support 200% zoom
### 6. Automated Testing
- **axe-core**: Industry standard accessibility engine
- **jest-axe**: Jest integration for unit tests
- **Lighthouse**: Google accessibility audits
- **Pa11y**: CI/CD accessibility testing
- **Testing Library**: Built-in accessibility queries
- **ESLint**: jsx-a11y plugin
### 7. Manual Testing
- **Keyboard Only**: Test without mouse
- **Screen Readers**: NVDA, JAWS, VoiceOver
- **Browser DevTools**: Accessibility tree inspection
- **Color Blindness**: Simulators
- **Zoom Testing**: 200% zoom verification
- **User Testing**: Real users with disabilities
## Workflow
### Phase 1: Accessibility Planning
1. **Define Requirements**
- WCAG level target (A, AA, AAA)?
- Legal requirements (Section 508, ADA)?
- User personas with disabilities?
- Priority features for accessibility?
2. **Audit Existing Code**
- Run automated tools (axe, Lighthouse)
- Manual keyboard testing
- Screen reader testing
- Color contrast checks
3. **Create Action Plan**
- Categorize issues (critical, high, medium, low)
- Estimate effort
- Prioritize fixes
- Set timeline
### Phase 2: Implementation
1. **Semantic HTML**
- Use correct elements
- Add ARIA only when needed
- Structure content logically
- Use landmarks
2. **Keyboard Support**
- Add keyboard handlers
- Manage focus
- Implement roving tabindex
- Add skip links
3. **Screen Reader Support**
- Add ARIA labels
- Implement live regions
- Test with real screen readers
- Fix announced text
4. **Visual Accessibility**
- Fix color contrast
- Add focus indicators
- Ensure text sizing
- Test with zoom
### Phase 3: Testing & Maintenance
1. **Automated Testing**
- Unit tests with jest-axe
- Integration tests
- CI/CD pipeline checks
- Regular audits
2. **Manual Testing**
- Keyboard navigation
- Screen reader testing
- User testing
- Browser compatibility
3. **Documentation**
- Accessibility statement
- Known issues
- Usage guidelines
- Testing procedures
## Accessibility Patterns
### Accessible Button
```tsx
// AccessibleButton.tsx
import { forwardRef, ButtonHTMLAttributes } from 'react'
interface AccessibleButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/**
* Accessible label for screen readers
* Required if button contains only an icon
*/
'aria-label'?: string
/**
* ID of element that labels this button
*/
'aria-labelledby'?: string
/**
* ID of element that describes this button
*/
'aria-describedby'?: string
/**
* Loading state (shows spinner, disables interaction)
*/
loading?: boolean
}
export const AccessibleButton = forwardRef<HTMLButtonElement, AccessibleButtonProps>(
({ children, loading, disabled, 'aria-label': ariaLabel, ...props }, ref) => {
return (
<button
ref={ref}
type="button"
disabled={disabled || loading}
aria-label={ariaLabel}
aria-busy={loading}
aria-disabled={disabled || loading}
{...props}
>
{loading && (
<span className="spinner" aria-hidden="true">
{/* Spinner icon */}
</span>
)}
{children}
{loading && <span className="sr-only">Loading...</span>}
</button>
)
}
)
AccessibleButton.displayName = 'AccessibleButton'
// Usage
<AccessibleButton onClick={handleClick}>
Save Changes
</AccessibleButton>
// Icon-only button (MUST have aria-label)
<AccessibleButton aria-label="Close dialog" onClick={onClose}>
<XIcon aria-hidden="true" />
</AccessibleButton>
```
### Accessible Modal/Dialog
```tsx
// AccessibleModal.tsx
import { useEffect, useRef, ReactNode } from 'react'
import { createPortal } from 'react-dom'
import { useFocusTrap } from './hooks/useFocusTrap'
import { useEscapeKey } from './hooks/useEscapeKey'
interface AccessibleModalProps {
isOpen: boolean
onClose: () => void
title: string
children: ReactNode
/**
* ID for the modal title (for aria-labelledby)
*/
titleId?: string
/**
* ID for the modal description (for aria-describedby)
*/
descriptionId?: string
}
export function AccessibleModal({
isOpen,
onClose,
title,
children,
titleId = 'modal-title',
descriptionId,
}: AccessibleModalProps) {
const modalRef = useRef<HTMLDivElement>(null)
const previousActiveElement = useRef<HTMLElement | null>(null)
// Trap focus inside modal
useFocusTrap(modalRef, isOpen)
// Close on Escape
useEscapeKey(onClose, isOpen)
useEffect(() => {
if (isOpen) {
// Store currently focused element
previousActiveElement.current = document.activeElement as HTMLElement
// Prevent body scroll
document.body.style.overflow = 'hidden'
// Focus modal
modalRef.current?.focus()
} else {
// Restore body scroll
document.body.style.overflow = ''
// Restore focus to previous element
previousActiveElement.current?.focus()
}
return () => {
document.body.style.overflow = ''
}
}, [isOpen])
if (!isOpen) return null
return createPortal(
<div
className="modal-overlay"
onClick={onClose}
role="presentation"
>
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={descriptionId}
className="modal-content"
onClick={(e) => e.stopPropagation()}
tabIndex={-1}
>
<div className="modal-header">
<h2 id={titleId}>{title}</h2>
<button
onClick={onClose}
aria-label="Close dialog"
className="modal-close"
>
<span aria-hidden="true">×</span>
</button>
</div>
<div className="modal-body" id={descriptionId}>
{children}
</div>
</div>
</div>,
document.body
)
}
// Usage
<AccessibleModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Confirm Action"
descriptionId="modal-desc"
>
<p id="modal-desc">Are you sure you want to delete this item?</p>
<button onClick={handleConfirm}>Confirm</button>
<button onClick={() => setIsOpen(false)}>Cancel</button>
</AccessibleModal>
```
### Focus Trap Hook
```tsx
// hooks/useFocusTrap.ts
import { useEffect, RefObject } from 'react'
const FOCUSABLE_ELEMENTS = [
'a[href]',
'button:not([disabled])',
'textarea:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
].join(', ')
export function useFocusTrap(ref: RefObject<HTMLElement>, isActive: boolean) {
useEffect(() => {
if (!isActive) return
const element = ref.current
if (!element) return
const focusableElements = element.querySelectorAll<HTMLElement>(FOCUSABLE_ELEMENTS)
const firstFocusable = focusableElements[0]
const lastFocusable = focusableElements[focusableElements.length - 1]
const handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return
if (e.shiftKey) {
// Shift + Tab
if (document.activeElement === firstFocusable) {
lastFocusable?.focus()
e.preventDefault()
}
} else {
// Tab
if (document.activeElement === lastFocusable) {
firstFocusable?.focus()
e.preventDefault()
}
}
}
element.addEventListener('keydown', handleTabKey)
return () => {
element.removeEventListener('keydown', handleTabKey)
}
}, [ref, isActive])
}
```
### Accessible Form with Live Validation
```tsx
// AccessibleForm.tsx
import { useState, useId, FormEvent } from 'react'
export function AccessibleForm() {
const [email, setEmail] = useState('')
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const emailId = useId()
const errorId = useId()
const successId = useId()
const validateEmail = (value: string) => {
if (!value) {
setError('Email is required')
return false
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
setError('Please enter a valid email address')
return false
}
setError('')
return true
}
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
if (validateEmail(email)) {
setSuccess('Form submitted successfully!')
// Submit form
}
}
return (
<form onSubmit={handleSubmit} noValidate>
<div className="form-field">
<label htmlFor={emailId}>
Email Address <span aria-label="required">*</span>
</label>
<input
id={emailId}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={() => validateEmail(email)}
aria-invalid={!!error}
aria-describedby={error ? errorId : undefined}
aria-required="true"
autoComplete="email"
/>
{error && (
<div
id={errorId}
role="alert"
aria-live="polite"
className="error-message"
>
{error}
</div>
)}
</div>
{success && (
<div
id={successId}
role="status"
aria-live="polite"
className="success-message"
>
{success}
</div>
)}
<button type="submit">Submit</button>
</form>
)
}
```
### Accessible Tabs
```tsx
// AccessibleTabs.tsx
import { useState, useRef, useEffect, KeyboardEvent } from 'react'
interface Tab {
id: string
label: string
content: React.ReactNode
}
interface AccessibleTabsProps {
tabs: Tab[]
defaultTab?: string
}
export function AccessibleTabs({ tabs, defaultTab }: AccessibleTabsProps) {
const [activeTab, setActiveTab] = useState(defaultTab || tabs[0].id)
const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map())
const handleKeyDown = (e: KeyboardEvent, currentIndex: number) => {
let newIndex = currentIndex
switch (e.key) {
case 'ArrowLeft':
newIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1
break
case 'ArrowRight':
newIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0
break
case 'Home':
newIndex = 0
break
case 'End':
newIndex = tabs.length - 1
break
default:
return
}
e.preventDefault()
const newTab = tabs[newIndex]
setActiveTab(newTab.id)
tabRefs.current.get(newTab.id)?.focus()
}
return (
<div className="tabs">
{/* Tab List */}
<div role="tablist" aria-label="Content tabs">
{tabs.map((tab, index) => {
const isActive = activeTab === tab.id
return (
<button
key={tab.id}
ref={(el) => {
if (el) tabRefs.current.set(tab.id, el)
}}
role="tab"
id={`tab-${tab.id}`}
aria-selected={isActive}
aria-controls={`panel-${tab.id}`}
tabIndex={isActive ? 0 : -1}
onClick={() => setActiveTab(tab.id)}
onKeyDown={(e) => handleKeyDown(e, index)}
className={isActive ? 'active' : ''}
>
{tab.label}
</button>
)
})}
</div>
{/* Tab Panels */}
{tabs.map((tab) => (
<div
key={tab.id}
role="tabpanel"
id={`panel-${tab.id}`}
aria-labelledby={`tab-${tab.id}`}
hidden={activeTab !== tab.id}
tabIndex={0}
>
{tab.content}
</div>
))}
</div>
)
}
```
### Screen Reader Only Text
```css
/* sr-only.css */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.sr-only-focusable:focus,
.sr-only-focusable:active {
position: static;
width: auto;
height: auto;
overflow: visible;
clip: auto;
white-space: normal;
}
```
```tsx
// Usage
<button>
<TrashIcon aria-hidden="true" />
<span className="sr-only">Delete item</span>
</button>
```
### Skip Links
```tsx
// SkipLinks.tsx
export function SkipLinks() {
return (
<div className="skip-links">
<a href="#main-content" className="skip-link">
Skip to main content
</a>
<a href="#navigation" className="skip-link">
Skip to navigation
</a>
<a href="#footer" className="skip-link">
Skip to footer
</a>
</div>
)
}
```
```css
/* Skip link styles */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
text-decoration: none;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
```
## Automated Testing
### Jest + jest-axe
```tsx
// Button.test.tsx
import { render } from '@testing-library/react'
import { axe, toHaveNoViolations } from 'jest-axe'
import { Button } from './Button'
expect.extend(toHaveNoViolations)
describe('Button Accessibility', () => {
it('should not have accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
it('should have accessible name', () => {
const { getByRole } = render(<Button>Click me</Button>)
expect(getByRole('button', { name: 'Click me' })).toBeInTheDocument()
})
it('icon-only button should have aria-label', async () => {
const { container } = render(
<Button aria-label="Close">
<XIcon />
</Button>
)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
it('disabled button should have aria-disabled', () => {
const { getByRole } = render(<Button disabled>Click me</Button>)
expect(getByRole('button')).toHaveAttribute('aria-disabled', 'true')
})
})
```
### Testing Library Accessibility Queries
```tsx
// Form.test.tsx
import { render, screen } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
describe('Form Accessibility', () => {
it('should have accessible form fields', () => {
render(<LoginForm />)
// Use accessible queries (getByRole, getByLabelText)
const emailInput = screen.getByLabelText(/email/i)
const passwordInput = screen.getByLabelText(/password/i)
const submitButton = screen.getByRole('button', { name: /log in/i })
expect(emailInput).toBeInTheDocument()
expect(passwordInput).toBeInTheDocument()
expect(submitButton).toBeInTheDocument()
})
it('should show validation errors with proper ARIA', async () => {
const user = userEvent.setup()
render(<LoginForm />)
const submitButton = screen.getByRole('button', { name: /log in/i })
await user.click(submitButton)
// Error should be announced to screen readers
const errorAlert = screen.getByRole('alert')
expect(errorAlert).toHaveTextContent(/email is required/i)
})
})
```
### Lighthouse CI
```yaml
# .lighthouserc.yml
ci:
collect:
numberOfRuns: 3
startServerCommand: 'npm run start'
url:
- 'http://localhost:3000'
assert:
preset: 'lighthouse:recommended'
assertions:
# Accessibility score must be >= 90
'categories:accessibility':
- error
- minScore: 0.9
# Specific accessibility checks
'aria-allowed-attr': 'error'
'aria-required-attr': 'error'
'aria-valid-attr': 'error'
'button-name': 'error'
'color-contrast': 'error'
'document-title': 'error'
'html-has-lang': 'error'
'image-alt': 'error'
'label': 'error'
'link-name': 'error'
```
## Best Practices
### Semantic HTML First
```tsx
// ❌ BAD - Div soup
<div onClick={handleClick}>Click me</div>
// ✅ GOOD - Use button
<button onClick={handleClick}>Click me</button>
```
### ARIA is a Last Resort
```tsx
// ❌ BAD - Unnecessary ARIA
<button role="button" aria-label="Submit">Submit</button>
// ✅ GOOD - Native semantics
<button>Submit</button>
```
### Always Provide Text Alternatives
```tsx
// ❌ BAD - Icon without label
<button><XIcon /></button>
// ✅ GOOD - Icon with label
<button aria-label="Close"><XIcon aria-hidden="true" /></button>
```
### Keyboard Accessibility
```tsx
// ✅ Ensure all interactive elements are keyboard accessible
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleClick()
}
}}
>
Custom button
</div>
```
### Focus Management
```tsx
// ✅ Manage focus in SPAs
useEffect(() => {
// Focus heading when page changes
headingRef.current?.focus()
}, [page])
```
## When to Use This Skill
Activate this skill when you need to:
- Build WCAG compliant components
- Add ARIA attributes correctly
- Implement keyboard navigation
- Create accessible modals/dialogs
- Fix accessibility issues
- Set up automated a11y testing
- Audit accessibility compliance
- Train team on accessibility
- Create accessible forms
- Implement screen reader support
## Integration with Agents
```typescript
// Agent Explore → Scan all components for a11y issues
// a11y-specialist → Apply fixes automatically
// Example workflow:
1. Agent finds: 50 buttons without accessible names
2. a11y-specialist adds aria-label to all
3. Agent verifies: All buttons now accessible
```
## Output Format
When implementing accessibility, provide:
1. **Accessible Component**: WCAG compliant code
2. **ARIA Documentation**: Explain ARIA usage
3. **Keyboard Support**: Document keyboard interactions
4. **Test Suite**: jest-axe tests included
5. **Manual Testing Guide**: How to test with screen readers
6. **Compliance Notes**: WCAG level achieved
Always build interfaces that are usable by everyone, regardless of ability.Related Skills
a11y-checker
Accessibility audit for CSS covering focus styles, color contrast, text sizing, screen reader support, and WCAG compliance. Provides actionable fixes. Use when auditing accessibility or fixing a11y issues.
a11y-ally
Comprehensive WCAG accessibility auditing with multi-tool testing (axe-core + pa11y + Lighthouse), TRUE PARALLEL execution with Promise.allSettled, graceful degradation, retry with backoff, context-aware remediation, learning integration, and video accessibility. Uses 3-tier browser cascade: Vibium → agent-browser → Playwright+Stealth.
applying-a11y-standards
Rules for ensuring Accessibility (a11y) across the Tourly app. Use to build inclusive interfaces.
documentation-specialist
文档专家。专注于技术文档编写、API 文档生成、README 优化和文档维护。提供清晰的文档结构、规范的格式和用户友好的内容。
terraform-specialist
Expert Terraform/OpenTofu specialist mastering advanced IaC automation, state management, and enterprise infrastructure patterns.
specialist-devops-infra
Engenharia DevOps para automação, CI/CD e infraestrutura como código
pronunciation-specialist
Scan lyrics for pronunciation risks, prevent Suno mispronunciations
fda-consultant-specialist
Senior FDA consultant and specialist for medical device companies including HIPAA compliance and requirement management. Provides FDA pathway expertise, QSR compliance, cybersecurity guidance, and regulatory submission support. Use for FDA submission planning, QSR compliance assessments, HIPAA evaluations, and FDA regulatory strategy development.
dpo-specialist
Expert Data Protection Officer (Datenschutzbeauftragter) with deep knowledge of EU GDPR (DSGVO), German BDSG, and ISO 27701:2025/2019 (PIMS). Specializes in smart integration with existing ISMS infrastructure using Data Reuse principles. Automatically activated when user asks about data protection, privacy, GDPR/DSGVO, BDSG, personal data, DPIA/DSFA, consent, data subject rights, ISO 27701, PIMS, or data breaches.
devops-specialist
DevOps 与运维专家。精通 CI/CD、容器化、编排、基础设施即代码、监控告警和自动化部署。用于构建高效、可靠的软件交付流水线和运维系统。
architecture-specialist
提供系统架构设计、技术选型、架构审查和组件设计能力。当需要设计新系统、重构现有架构或进行架构审查时使用。
agent-search-specialist
Expert search specialist mastering advanced information retrieval, query optimization, and knowledge discovery. Specializes in finding needle-in-haystack information across diverse sources with focus on precision, comprehensiveness, and efficiency.