| name | a11y-specialist |
| description | 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 |
| allowed-tools | ["Read","Write","Edit","Bash","Glob","Grep","Task"] |
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
-
Define Requirements
- WCAG level target (A, AA, AAA)?
- Legal requirements (Section 508, ADA)?
- User personas with disabilities?
- Priority features for accessibility?
-
Audit Existing Code
- Run automated tools (axe, Lighthouse)
- Manual keyboard testing
- Screen reader testing
- Color contrast checks
-
Create Action Plan
- Categorize issues (critical, high, medium, low)
- Estimate effort
- Prioritize fixes
- Set timeline
Phase 2: Implementation
-
Semantic HTML
- Use correct elements
- Add ARIA only when needed
- Structure content logically
- Use landmarks
-
Keyboard Support
- Add keyboard handlers
- Manage focus
- Implement roving tabindex
- Add skip links
-
Screen Reader Support
- Add ARIA labels
- Implement live regions
- Test with real screen readers
- Fix announced text
-
Visual Accessibility
- Fix color contrast
- Add focus indicators
- Ensure text sizing
- Test with zoom
Phase 3: Testing & Maintenance
-
Automated Testing
- Unit tests with jest-axe
- Integration tests
- CI/CD pipeline checks
- Regular audits
-
Manual Testing
- Keyboard navigation
- Screen reader testing
- User testing
- Browser compatibility
-
Documentation
- Accessibility statement
- Known issues
- Usage guidelines
- Testing procedures
Accessibility Patterns
Accessible Button
import { forwardRef, ButtonHTMLAttributes } from 'react'
interface AccessibleButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
'aria-label'?: string
'aria-labelledby'?: string
'aria-describedby'?: string
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}
{}
>
{loading && (
{/* Spinner icon */}
)}
{children}
{loading && Loading...}
)
}
)
. =
< onClick={handleClick}>
</>
Accessible Modal/Dialog
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
titleId?: string
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)
useFocusTrap(modalRef, isOpen)
useEscapeKey(onClose, isOpen)
( {
(isOpen) {
previousActiveElement. = .
... =
modalRef.?.()
} {
... =
previousActiveElement.?.()
}
{
... =
}
}, [isOpen])
(!isOpen)
(
,
.
)
}
<
isOpen={isOpen}
onClose={ ()}
title=
descriptionId=
>
</>
Focus Trap Hook
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
(e.) {
(. === firstFocusable) {
lastFocusable?.()
e.()
}
} {
(. === lastFocusable) {
firstFocusable?.()
e.()
}
}
}
element.(, handleTabKey)
{
element.(, handleTabKey)
}
}, [ref, isActive])
}
Accessible Form with Live Validation
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()
((email)) {
()
}
}
(
)
}
Accessible Tabs
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
:
newIndex = currentIndex < tabs. - ? currentIndex + :
:
newIndex =
:
newIndex = tabs. -
:
}
e.()
newTab = tabs[newIndex]
(newTab.)
tabRefs..(newTab.)?.()
}
(
)
}
Screen Reader Only Text
.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;
}
<button>
<TrashIcon aria-hidden="true" />
<span className="sr-only">Delete item</span>
</button>
Skip Links
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>
)
}
.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
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 } = (
)
results = (container)
(results).()
})
(, {
{ getByRole } = ()
(()).(, )
})
})
Testing Library Accessibility Queries
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 />)
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', { : })
user.(submitButton)
errorAlert = screen.()
(errorAlert).()
})
})
Lighthouse CI
ci:
collect:
numberOfRuns: 3
startServerCommand: 'npm run start'
url:
- 'http://localhost:3000'
assert:
preset: 'lighthouse:recommended'
assertions:
'categories:accessibility':
- error
- minScore: 0.9
'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
<div onClick={handleClick}>Click me</div>
<button onClick={handleClick}>Click me</button>
ARIA is a Last Resort
<button role="button" aria-label="Submit">Submit</button>
<button>Submit</button>
Always Provide Text Alternatives
<button><XIcon /></button>
<button aria-label="Close"><XIcon aria-hidden="true" /></button>
Keyboard Accessibility
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleClick()
}
}}
>
Custom button
</div>
Focus Management
useEffect(() => {
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
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:
- Accessible Component: WCAG compliant code
- ARIA Documentation: Explain ARIA usage
- Keyboard Support: Document keyboard interactions
- Test Suite: jest-axe tests included
- Manual Testing Guide: How to test with screen readers
- Compliance Notes: WCAG level achieved
Always build interfaces that are usable by everyone, regardless of ability.