Loading...
Loading...
Build accessible UIs with semantic HTML, ARIA attributes, keyboard navigation, color contrast, and screen reader support. Apply when creating or modifying frontend components, forms, interactive elements, or any UI that needs WCAG compliance.
npx skill4agent add maxritter/claude-codepro standards-accessibility<!-- Navigation -->
<nav><a href="/about">About</a></nav>
<!-- Buttons that perform actions -->
<button onClick="{handleSubmit}">Submit</button>
<!-- Links that navigate -->
<a href="/profile">View Profile</a>
<!-- Main content area -->
<main><article>...</article></main>
<!-- Form structure -->
<form>
<label for="email">Email</label>
<input id="email" type="email" />
</form><!-- BAD - div/span without semantic meaning -->
<div onClick="{navigate}">Go to page</div>
<span onClick="{handleClick}">Submit</span><button><a><nav><main><header><footer><aside><article><section>// Native elements are keyboard accessible by default
<button onClick={handleClick}>Click me</button>
// Custom interactive elements need tabIndex
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick();
}
}}
>
Custom button
</div>
// Focus styles must be visible
button:focus {
outline: 2px solid blue;
outline-offset: 2px;
}tabIndex<!-- Explicit label association -->
<label for="username">Username</label>
<input id="username" type="text" />
<!-- Implicit label wrapping -->
<label>
Email
<input type="email" />
</label>
<!-- aria-label for icon-only buttons -->
<button aria-label="Close dialog">
<CloseIcon />
</button>
<!-- aria-describedby for help text -->
<label for="password">Password</label>
<input id="password" type="password" aria-describedby="password-help" />
<span id="password-help">Must be at least 8 characters</span>idfortypearia-labelaria-labelledbyaria-describedby<!-- Informative images -->
<img src="chart.png" alt="Sales increased 40% in Q4 2024" />
<!-- Functional images (buttons, links) -->
<a href="/search">
<img src="search-icon.svg" alt="Search" />
</a>
<!-- Decorative images -->
<img src="decoration.png" alt="" />
{/* or */}
<img src="decoration.png" role="presentation" />
<!-- Complex images need longer descriptions -->
<img
src="architecture.png"
alt="System architecture diagram"
aria-describedby="arch-description"
/>
<div id="arch-description">
The system consists of three layers: frontend React app,
Node.js API server, and PostgreSQL database...
</div>alt=""// BAD - color only
<span style={{color: 'red'}}>Error</span>
// GOOD - color + icon + text
<span style={{color: 'red'}}>
<ErrorIcon aria-hidden="true" />
Error: Invalid email format
</span>
// BAD - color-coded status
<div style={{backgroundColor: status === 'active' ? 'green' : 'red'}} />
// GOOD - color + text label
<div>
<StatusBadge color={status === 'active' ? 'green' : 'red'}>
{status === 'active' ? 'Active' : 'Inactive'}
</StatusBadge>
</div>// Roles for custom components
<div role="dialog" aria-modal="true">
<h2 id="dialog-title">Confirm Action</h2>
<div aria-describedby="dialog-desc">...</div>
</div>
// States and properties
<button aria-expanded={isOpen} aria-controls="menu">
Menu
</button>
<ul id="menu" hidden={!isOpen}>...</ul>
// Live regions for dynamic content
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
// Hide decorative elements
<span aria-hidden="true">→</span><button role="link">aria-labelaria-labelledbyaria-describedbyaria-expandedaria-hiddenaria-live<h1>Page Title</h1>
<h2>Section 1</h2>
<h3>Subsection 1.1</h3>
<h3>Subsection 1.2</h3>
<h2>Section 2</h2>
<h3>Subsection 2.1</h3><h1>/* Separate semantic level from visual appearance */
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.5rem;
}
/* If you need h3 to look like h1 */
.h3-large {
font-size: 2rem;
}function Modal({ isOpen, onClose, children }) {
const modalRef = useRef();
useEffect(() => {
if (isOpen) {
// Save previously focused element
const previousFocus = document.activeElement;
// Move focus to modal
modalRef.current?.focus();
// Trap focus within modal
// (use library like focus-trap-react)
return () => {
// Restore focus when modal closes
previousFocus?.focus();
};
}
}, [isOpen]);
return (
<div ref={modalRef} role="dialog" aria-modal="true" tabIndex={-1}>
{children}
<button onClick={onClose}>Close</button>
</div>
);
}// Announce content changes to screen readers
<div aria-live="polite">{loading ? "Loading..." : `Loaded ${items.length} items`}</div>;
// Move focus to new content after navigation
function handlePageChange(newPage) {
loadPage(newPage);
// Focus the main heading of new content
document.querySelector("h1")?.focus();
}// BAD
<div onClick={handleClick}>Submit</div>
// GOOD
<button onClick={handleClick}>Submit</button>// BAD
<input type="text" placeholder="Username" />
// GOOD
<label for="username">Username</label>
<input id="username" type="text" />/* BAD */
button:focus {
outline: none;
}
/* GOOD - provide alternative indicator */
button:focus {
outline: 2px solid blue;
outline-offset: 2px;
}// BAD - button already has button role
<button role="button">Click</button>
// GOOD - use native semantics
<button>Click</button>// BAD - no keyboard support
<div onClick={handleClick}>Custom button</div>
// GOOD - full keyboard support
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
}}
>
Custom button
</div>