From frontend-developer
Audits web accessibility for WCAG compliance, checks screen reader support, and fixes issues like alt text, color contrast, ARIA labels, keyboard navigation, and semantic HTML.
How this skill is triggered — by the user, by Claude, or both
Slash command
/frontend-developer:accessibility-checkerThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Validate web accessibility and ensure WCAG compliance.
Validate web accessibility and ensure WCAG compliance.
Use semantic HTML, add ARIA labels, ensure keyboard navigation, test color contrast, validate with axe or Lighthouse.
Level A (minimum):
Level AA (recommended):
Level AAA (enhanced):
1. Missing alt text:
// Bad
<img src="logo.png" />
// Good
<img src="logo.png" alt="Company Logo" />
// Decorative images
<img src="decoration.png" alt="" />
2. Poor color contrast:
/* Bad: 2.5:1 contrast */
color: #777;
background: #fff;
/* Good: 4.5:1 contrast (AA) */
color: #595959;
background: #fff;
/* Better: 7:1 contrast (AAA) */
color: #333;
background: #fff;
3. Missing form labels:
// Bad
<input type="text" placeholder="Name" />
// Good
<label htmlFor="name">Name</label>
<input type="text" id="name" />
// Or with aria-label
<input type="text" aria-label="Name" />
4. No keyboard navigation:
// Bad: onClick on div
<div onClick={handleClick}>Click me</div>
// Good: Use button
<button onClick={handleClick}>Click me</button>
// Or make div focusable
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyPress={(e) => e.key === 'Enter' && handleClick()}
>
Click me
</div>
5. Missing ARIA labels:
// Bad: Icon button without label
<button><CloseIcon /></button>
// Good: Add aria-label
<button aria-label="Close dialog">
<CloseIcon />
</button>
Use proper elements:
// Bad
<div onClick={handleClick}>Submit</div>
// Good
<button onClick={handleClick}>Submit</button>
// Bad
<div className="heading">Title</div>
// Good
<h1>Title</h1>
Heading hierarchy:
// Bad: Skipping levels
<h1>Page Title</h1>
<h3>Section</h3>
// Good: Proper hierarchy
<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-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:
// Announce updates to screen readers
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
// For urgent updates
<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>
Tab order:
// Use tabIndex to control focus 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(() => {
// Focus close button when dialog opens
closeButtonRef.current?.focus();
// Trap focus in dialog
const handleTab = (e) => {
if (e.key === 'Tab') {
// Implement focus trap
}
};
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);
}, []);
Skip links:
<a href="#main-content" className="skip-link">
Skip to main content
</a>
<main id="main-content">
{/* Content */}
</main>
// CSS
.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:
// 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;
}
// Usage
<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>
);
}
Check contrast ratios:
Tools:
Don't rely on color alone:
// Bad: Only color indicates error
<input style={{ borderColor: 'red' }} />
// Good: Color + icon + text
<div>
<input aria-invalid="true" aria-describedby="error" />
<span id="error">
<ErrorIcon /> Email is required
</span>
</div>
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>
Automated testing:
# Install axe-core
npm install --save-dev @axe-core/react
# Use in tests
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:
Screen readers:
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>
);
}
Perceivable:
Operable:
Understandable:
Robust:
Start accessible:
Progressive enhancement:
Regular testing:
npx claudepluginhub p/armanzeroeight-frontend-developer-plugins-frontend-developerAutomatically checks code and elements for accessibility issues when users ask about WCAG compliance or accessibility. Performs focused checks on components, forms, or pages.
Audits web UIs for WCAG 2.1 AA compliance, checking semantic HTML, ARIA, keyboard navigation, color contrast, and screen reader compatibility. Provides checklists, fixes, and tools like axe-core.
Reviews digital products against WCAG 2.2 AA with automated, keyboard, screen reader, and contrast tests. Use for pre-merge a11y audits, form/component validation, and defining accessibility requirements.