en un clic
frontend-web-accessibility
Building accessible web applications
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Menu
Building accessible web applications
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Basé sur la classification professionnelle SOC
Index of Build Systems Skills
Coordination patterns for distributed dataflow systems including barriers, epochs, and distributed snapshots
Windowing, sessionization, time-series aggregation, and late data handling for streaming systems
Comprehensive guide to GNU Debugger (GDB) for debugging C/C++/Rust programs. Covers breakpoints, stack traces, variable inspection, TUI mode, .gdbinit customization, Python scripting, remote debugging, and core file analysis.
Paxos consensus algorithm including Basic Paxos, Multi-Paxos, roles, phases, and practical implementations
Gossip protocols for disseminating information, failure detection, and eventual consistency in large-scale distributed systems
| name | frontend-web-accessibility |
| description | Building accessible web applications |
Scope: WCAG 2.1 AA, ARIA, keyboard navigation, screen readers, color contrast Lines: ~300 Last Updated: 2025-10-18
Activate this skill when:
Perceivable - Information must be presentable to users Operable - UI must be operable by all users Understandable - Information must be understandable Robust - Content must work across technologies
Level A - Minimum (basic accessibility) Level AA - Recommended (target for most sites) Level AAA - Enhanced (not always achievable)
Target: WCAG 2.1 AA for production applications.
// ❌ Bad: Divs for everything
<div onClick={handleClick}>Click me</div>
<div>Important message</div>
// ✅ Good: Semantic elements
<button onClick={handleClick}>Click me</button>
<main>Important message</main>
<body>
<header>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Page Title</h1>
<section>
<h2>Section Title</h2>
<p>Content...</p>
</section>
</article>
<aside>
<h2>Related Links</h2>
<ul>...</ul>
</aside>
</main>
<footer>
<p>© 2025 Company</p>
</footer>
</body>
// ❌ Bad: Skipping levels
<h1>Page Title</h1>
<h3>Subsection</h3>
<h5>Detail</h5>
// ✅ Good: Proper hierarchy
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
Rules:
<h1> per pageFirst Rule of ARIA: Don't use ARIA if you can use native HTML.
// ❌ Bad: ARIA on native element
<div role="button" tabIndex={0} onClick={...}>Click</div>
// ✅ Good: Native element
<button onClick={...}>Click</button>
aria-label - Accessible name
<button aria-label="Close dialog">
<XIcon />
</button>
aria-labelledby - Reference to label
<div role="dialog" aria-labelledby="dialog-title">
<h2 id="dialog-title">Confirm Delete</h2>
...
</div>
aria-describedby - Additional description
<input
type="password"
aria-describedby="password-hint"
/>
<p id="password-hint">Must be at least 8 characters</p>
aria-live - Announce dynamic content
<div aria-live="polite" aria-atomic="true">
{status}
</div>
aria-expanded - Disclosure state
<button
aria-expanded={isOpen}
aria-controls="dropdown-menu"
onClick={() => setIsOpen(!isOpen)}
>
Menu
</button>
<ul id="dropdown-menu" hidden={!isOpen}>
<li>Item 1</li>
<li>Item 2</li>
</ul>
aria-hidden - Hide from screen readers
<span aria-hidden="true">★</span>
<span className="sr-only">5 stars</span>
// Navigation landmark
<nav role="navigation">...</nav>
// Search landmark
<form role="search">...</form>
// Alert (announces to screen reader)
<div role="alert">Error: Form submission failed</div>
// Tab interface
<div role="tablist">
<button role="tab" aria-selected={true}>Tab 1</button>
<button role="tab" aria-selected={false}>Tab 2</button>
</div>
<div role="tabpanel">Content</div>
Tab Order - Follow DOM order
// ❌ Bad: Breaking tab order with CSS
<div style={{ display: 'flex', flexDirection: 'column-reverse' }}>
<button>Second (visually first)</button>
<button>First (visually second)</button>
</div>
// ✅ Good: DOM order matches visual order
<div style={{ display: 'flex', flexDirection: 'column' }}>
<button>First</button>
<button>Second</button>
</div>
tabIndex
// 0 = Natural tab order
<div tabIndex={0}>Focusable</div>
// -1 = Programmatically focusable (not in tab order)
<div tabIndex={-1}>Not in tab order</div>
// Positive numbers = Custom tab order (avoid!)
function KeyboardAccessibleButton({ onClick, children }: {
onClick: () => void;
children: React.ReactNode;
}) {
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
};
return (
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={handleKeyDown}
>
{children}
</div>
);
}
// ✅ Better: Just use <button>
<button onClick={onClick}>{children}</button>
import { useEffect, useRef } from 'react';
function Modal({ isOpen, onClose, children }: {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}) {
const dialogRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isOpen) return;
const dialog = dialogRef.current;
if (!dialog) return;
// Focus first focusable element
const focusableElements = dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
firstElement?.focus();
// Trap focus inside modal
const handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement?.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement?.focus();
}
}
};
// Close on Escape
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
dialog.addEventListener('keydown', handleTabKey);
dialog.addEventListener('keydown', handleEscape);
return () => {
dialog.removeEventListener('keydown', handleTabKey);
dialog.removeEventListener('keydown', handleEscape);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
>
<h2 id="dialog-title">Dialog Title</h2>
{children}
</div>
);
}
// CSS approach
<style>
.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;
}
</style>
<button>
<XIcon aria-hidden="true" />
<span className="sr-only">Close</span>
</button>
function Toast({ message }: { message: string }) {
return (
<div
role="status"
aria-live="polite"
aria-atomic="true"
>
{message}
</div>
);
}
function ErrorAlert({ error }: { error: string }) {
return (
<div
role="alert"
aria-live="assertive"
aria-atomic="true"
>
{error}
</div>
);
}
aria-live values:
off - No announcementspolite - Announce when idle (default)assertive - Interrupt and announce immediately// ❌ Bad: No label
<input type="text" placeholder="Email" />
// ✅ Good: Explicit label
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// ✅ Good: Implicit label
<label>
Email
<input type="email" />
</label>
// ✅ Good: aria-label (when visual label not desired)
<input type="search" aria-label="Search products" />
Normal text (< 18pt): 4.5:1 minimum Large text (≥ 18pt or 14pt bold): 3:1 minimum
// ❌ Bad: Insufficient contrast
<div style={{ color: '#999', backgroundColor: '#fff' }}>
Text (2.8:1 - fails AA)
</div>
// ✅ Good: Sufficient contrast
<div style={{ color: '#767676', backgroundColor: '#fff' }}>
Text (4.5:1 - passes AA)
</div>
Tools:
// ❌ Bad: Color only
<span style={{ color: 'red' }}>Error</span>
// ✅ Good: Color + icon + text
<span style={{ color: 'red' }}>
<ErrorIcon aria-hidden="true" />
Error: Field is required
</span>
// ❌ Bad: Missing alt
<img src="logo.png" />
// ❌ Bad: Redundant alt
<img src="photo.jpg" alt="Image of a photo" />
// ✅ Good: Descriptive alt
<img src="logo.png" alt="Company Logo" />
// ✅ Good: Decorative image
<img src="decoration.png" alt="" />
Alt text guidelines:
alt="") for decorative images<video controls>
<source src="video.mp4" type="video/mp4" />
<track
kind="captions"
src="captions.vtt"
srcLang="en"
label="English"
default
/>
</video>
<button
type="button"
onClick={handleClick}
disabled={isDisabled}
aria-label="Close dialog"
>
<XIcon aria-hidden="true" />
</button>
// ❌ Bad: Non-descriptive link
<a href="/report.pdf">Click here</a>
// ✅ Good: Descriptive link
<a href="/report.pdf">Download 2025 Annual Report (PDF, 2MB)</a>
// ✅ Good: External link
<a href="https://example.com" target="_blank" rel="noopener noreferrer">
Visit Example
<span className="sr-only">(opens in new tab)</span>
</a>
import { useState } from 'react';
function Dropdown({ label, items }: {
label: string;
items: Array<{ id: string; label: string; onClick: () => void }>;
}) {
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="none">
<button
role="menuitem"
onClick={() => {
item.onClick();
setIsOpen(false);
}}
>
{item.label}
</button>
</li>
))}
</ul>
)}
</div>
);
}
// Jest + Testing Library
import { render } from '@testing-library/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();
});
Keyboard navigation:
Screen reader testing:
Browser DevTools:
[ ] Semantic HTML (header, nav, main, footer)
[ ] Heading hierarchy (h1 → h6, no skipping)
[ ] Alt text for images
[ ] Labels for form inputs
[ ] Keyboard navigation (Tab, Enter, Space, Esc)
[ ] Focus indicators (visible outline)
[ ] Color contrast (4.5:1 for text)
[ ] ARIA labels for icon buttons
[ ] aria-live for dynamic content
[ ] Focus trap in modals
[ ] Skip navigation link
[ ] No reliance on color alone
[ ] Descriptive link text
[ ] Captions for video
// Button
<button aria-label="Close">×</button>
// Toggle
<button aria-pressed={isActive}>Toggle</button>
// Expandable
<button aria-expanded={isOpen} aria-controls="content">Expand</button>
// Live region
<div aria-live="polite" role="status">{message}</div>
// Dialog
<div role="dialog" aria-modal="true" aria-labelledby="title">
// Tab
<div role="tablist">
<button role="tab" aria-selected={true}>Tab 1</button>
</div>
<div role="tabpanel">Content</div>
❌ Div/span as button: Use <button>
✅ Native semantics
❌ No focus indicators: Users can't see where they are ✅ Visible focus styles
❌ Positive tabIndex: Breaks natural tab order ✅ Use 0 or -1 only
❌ Color-only indicators: Not accessible to colorblind ✅ Use color + icon/text
❌ Missing alt text: Screen readers can't describe images ✅ Descriptive alt text
❌ Auto-playing media: Disorienting, annoying ✅ User-controlled playback
react-component-patterns.md - Custom hook patternsreact-form-handling.md - Accessible form patternsnextjs-app-router.md - Metadata, SEOfrontend-performance.md - Performance affects accessibilityLocation: skills/frontend/web-accessibility/resources/
Comprehensive 1,900+ line reference covering:
check_accessibility.py (Python, executable)
./check_accessibility.py <url> [--standard wcag2aa] [--json]analyze_aria.py (Python, executable)
./analyze_aria.py <path> [--recursive] [--json]test_keyboard_nav.js (Node.js/Puppeteer, executable)
./test_keyboard_nav.js <url> [--json]react/accessible-modal.tsx
react/accessible-form.tsx
react/accessible-dropdown.tsx
html/semantic-examples.html
css/focus-visible.css
Last Updated: 2025-10-27 Format Version: 1.0 (Atomic)