| name | accessibility-checker |
| description | Validate WCAG compliance, check screen reader support, and audit accessibility issues. Use when ensuring accessibility, fixing WCAG violations, or implementing inclusive design. |
Accessibility Checker
Validate web accessibility and ensure WCAG compliance.
Quick Start
Use semantic HTML, add ARIA labels, ensure keyboard navigation, test color contrast, validate with axe or Lighthouse.
Instructions
WCAG Compliance Levels
Level A (minimum):
- Basic accessibility
- Must meet for legal compliance
Level AA (recommended):
- Standard for most websites
- Includes contrast requirements
- Government sites requirement
Level AAA (enhanced):
- Highest level
- Not required for all content
- Best practices
Common Accessibility Issues
1. Missing alt text:
<img src="logo.png" />
<img src="logo.png" alt="Company Logo" />
<img src="decoration.png" alt="" />
2. Poor color contrast:
color: #777;
background: #fff;
color: #595959;
background: #fff;
color: #333;
background: #fff;
3. Missing form labels:
<input type="text" placeholder="Name" />
<label htmlFor="name">Name</label>
<input type="text" id="name" />
<input type="text" aria-label="Name" />
4. No keyboard navigation:
<div onClick={handleClick}>Click me</div>
<button onClick={handleClick}>Click me</button>
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyPress={(e) => e.key === 'Enter' && handleClick()}
>
Click me
</div>
5. Missing ARIA labels:
<button><CloseIcon /></button>
<button aria-label="Close dialog">
<CloseIcon />
</button>
Semantic HTML
Use proper elements:
<div onClick={handleClick}>Submit</div>
<button onClick={handleClick}>Submit</button>
<div className="heading">Title</div>
<h1>Title</h1>
Heading hierarchy:
<h1>Page Title</h1>
<h3>Section</h3>
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
Landmarks:
<header>
<nav aria-label="Main navigation">
{/* Navigation links */}
</nav>
</header>
<main>
<article>
{/* Main content */}
</article>
<aside>
{/* Sidebar */}
</aside>
</main>
<footer>
{/* Footer content */}
</footer>
ARIA Attributes
aria-label:
<button aria-label="Close">
<X />
</button>
aria-labelledby:
<h2 id="dialog-title">Confirm Action</h2>
<div role="dialog" aria-labelledby="dialog-title">
{/* Dialog content */}
</div>
aria-describedby:
<input
type="password"
aria-describedby="password-hint"
/>
<span id="password-hint">
Must be at least 8 characters
</span>
aria-live:
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
<div aria-live="assertive">
{errorMessage}
</div>
aria-expanded:
<button
aria-expanded={isOpen}
aria-controls="menu"
onClick={toggleMenu}
>
Menu
</button>
<div id="menu" hidden={!isOpen}>
{/* Menu items */}
</div>
Keyboard Navigation
Tab order:
<button tabIndex={0}>First</button>
<button tabIndex={0}>Second</button>
<button tabIndex={-1}>Not in tab order</button>
Focus management:
function Dialog({ onClose }) {
const closeButtonRef = useRef();
useEffect(() => {
closeButtonRef.current?.focus();
const handleTab = (e) => {
if (e.key === 'Tab') {
}
};
document.addEventListener('keydown', handleTab);
return () => document.removeEventListener('keydown', handleTab);
}, []);
return (
<div role="dialog" aria-modal="true">
<button ref={closeButtonRef} onClick={onClose}>
Close
</button>
</div>
);
}
Keyboard shortcuts:
useEffect(() => {
const handleKeyPress = (e) => {
if (e.key === 'Escape') {
closeDialog();
}
if (e.key === '/' && e.ctrlKey) {
openSearch();
}
};
document.addEventListener('keydown', handleKeyPress);
return () => document.removeEventListener('keydown', handleKeyPress);
}, []);
Screen Reader Support
Skip links:
<a href="#main-content" className="skip-link">
Skip to main content
</a>
<main id="main-content">
{/* Content */}
</main>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
Visually hidden 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;
}
<button>
<TrashIcon />
<span className="sr-only">Delete item</span>
</button>
Announce dynamic content:
function Toast({ message }) {
return (
<div
role="status"
aria-live="polite"
aria-atomic="true"
>
{message}
</div>
);
}
Color Contrast
Check contrast ratios:
- Normal text: 4.5:1 (AA), 7:1 (AAA)
- Large text (18pt+): 3:1 (AA), 4.5:1 (AAA)
- UI components: 3:1
Tools:
- WebAIM Contrast Checker
- Chrome DevTools
- Lighthouse
Don't rely on color alone:
<input style={{ borderColor: 'red' }} />
<div>
<input aria-invalid="true" aria-describedby="error" />
<span id="error">
<ErrorIcon /> Email is required
</span>
</div>
Forms Accessibility
Labels:
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
required
aria-required="true"
/>
Error messages:
<input
type="email"
aria-invalid={hasError}
aria-describedby={hasError ? "email-error" : undefined}
/>
{hasError && (
<span id="email-error" role="alert">
Please enter a valid email
</span>
)}
Fieldsets:
<fieldset>
<legend>Shipping Address</legend>
<label htmlFor="street">Street</label>
<input type="text" id="street" />
</fieldset>
Testing Tools
Automated testing:
npm install --save-dev @axe-core/react
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('should have no accessibility violations', async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Browser extensions:
- axe DevTools
- WAVE
- Lighthouse
Screen readers:
- NVDA (Windows, free)
- JAWS (Windows)
- VoiceOver (Mac, built-in)
- TalkBack (Android)
Common Patterns
Modal dialog:
function Modal({ isOpen, onClose, title, children }) {
const modalRef = useRef();
useEffect(() => {
if (isOpen) {
modalRef.current?.focus();
document.body.style.overflow = 'hidden';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
ref={modalRef}
tabIndex={-1}
>
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose} aria-label="Close dialog">
Close
</button>
</div>
);
}
Dropdown menu:
function Dropdown({ label, items }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button
aria-haspopup="true"
aria-expanded={isOpen}
onClick={() => setIsOpen(!isOpen)}
>
{label}
</button>
{isOpen && (
<ul role="menu">
{items.map(item => (
<li key={item.id} role="menuitem">
<button onClick={item.onClick}>
{item.label}
</button>
</li>
))}
</ul>
)}
</div>
);
}
Tabs:
function Tabs({ tabs }) {
const [activeTab, setActiveTab] = useState(0);
return (
<div>
<div role="tablist">
{tabs.map((tab, index) => (
<button
key={tab.id}
role="tab"
aria-selected={activeTab === index}
aria-controls={`panel-${tab.id}`}
onClick={() => setActiveTab(index)}
>
{tab.label}
</button>
))}
</div>
{tabs.map((tab, index) => (
<div
key={tab.id}
role="tabpanel"
id={`panel-${tab.id}`}
hidden={activeTab !== index}
>
{tab.content}
</div>
))}
</div>
);
}
Accessibility Checklist
Perceivable:
Operable:
Understandable:
Robust:
Best Practices
Start accessible:
- Use semantic HTML first
- Add ARIA only when needed
- Test with keyboard
- Test with screen reader
Progressive enhancement:
- Core functionality works without JS
- Enhanced experience with JS
- Graceful degradation
Regular testing:
- Automated tests (axe)
- Manual keyboard testing
- Screen reader testing
- User testing with disabled users