원클릭으로
frontend-web-accessibility
Building accessible web applications
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Building accessible web applications
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
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)