--- id: frontend-a11y-modern title: Modern Accessibility — ARIA / focus / keyboard category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [frontend, accessibility, vibe-coding] tech_stack: { language: "TS / React", applicable_to: ["Frontend"] } applied_in: [] aliases: [a11y, accessibility, ARIA, focus management, keyboard nav, screen reader, WCAG] --- # Modern Accessibility (a11y) > 사용자 의 ~15% 가 disability. **WCAG 2.2, ARIA, focus, keyboard, screen reader**. ## 📖 핵심 개념 - Native HTML 가 best (button, anchor, label, ...). - ARIA 가 last resort. - Keyboard 가 first-class. - Screen reader test. ## 💻 코드 패턴 ### Semantic HTML ```html
Submit
``` → 90% 의 a11y 가 native. ### Heading hierarchy ```html

Page title

Section 1

Subsection

Section 2

``` → Skip level 안 됨. ### Label ```html ``` → Screen reader 가 명확. ### ARIA (last resort) ```html
Custom button
``` → Native 가 안 가능 시 만. ### ARIA attributes ```html At least 8 characters
Status update
Error: ...
``` ### Focus management ```ts // Trap (modal) function trapFocus(container: HTMLElement) { const focusable = container.querySelectorAll('a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex="-1"])'); const first = focusable[0] as HTMLElement; const last = focusable[focusable.length - 1] as HTMLElement; container.addEventListener('keydown', (e) => { if (e.key !== 'Tab') return; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } }); first.focus(); } ``` → Modal / dialog 안 만 tab. ### Restore focus ```ts function openModal() { const previousFocus = document.activeElement as HTMLElement; // ... show modal // On close previousFocus.focus(); } ``` ### Skip link ```html ``` → Screen reader / keyboard user 가 navigation skip. ### Keyboard navigation ``` Tab: 다음 focusable. Shift+Tab: 옛. Enter / Space: button. Escape: modal close. Arrow: list / menu / radio. ``` ```tsx function Menu({ items }: { items: string[] }) { const [activeIdx, setActiveIdx] = useState(0); const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'ArrowDown') setActiveIdx(i => Math.min(items.length - 1, i + 1)); if (e.key === 'ArrowUp') setActiveIdx(i => Math.max(0, i - 1)); if (e.key === 'Enter') select(items[activeIdx]); }; // ... } ``` ### Focus visible ```css /* :focus 는 mouse 도 */ button:focus { outline: 2px solid blue; } /* :focus-visible = keyboard 만 */ button:focus-visible { outline: 2px solid blue; } button:focus:not(:focus-visible) { outline: none; } ``` ### Color contrast ``` WCAG AA: 4.5:1 (normal text). WCAG AAA: 7:1. → Tools: WebAIM contrast checker. Tailwind 도 contrast warning. ``` ### Reduced motion ```css @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } ``` → Vestibular disorder. ### Reduced motion (JS) ```ts const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; ``` ### Screen reader test ``` macOS: VoiceOver (Cmd + F5). Windows: NVDA (free) / JAWS. iOS: VoiceOver. Android: TalkBack. → Manual test 가 진짜. ``` ### Auto test (axe-core) ```ts import { axe } from 'jest-axe'; test('accessible', async () => { const { container } = render(); expect(await axe(container)).toHaveNoViolations(); }); ``` → Automated audit. ### Lighthouse / Pa11y ```bash lighthouse https://example.com --only-categories=accessibility pa11y https://example.com ``` → CI gate. ### React (a11y) ```tsx // React 의 accessible component // Form
{error && }
``` ### Radix / shadcn (a11y baked in) ```tsx import * as Dialog from '@radix-ui/react-dialog'; Open ... ``` → Focus trap, Escape, ARIA 자동. → [[Frontend_shadcn_Radix_Patterns]]. ### Image alt ```html Person typing on laptop ``` ### Form error ```html Required ``` ### Live region ```html
Item added to cart
Network error
``` → Dynamic update 가 announce. ### Mobile a11y ``` iOS: - VoiceOver, accessibilityLabel. - Dynamic Type (font size). Android: - TalkBack, contentDescription. - Talkback gesture. React Native: - accessibilityLabel. - accessibilityHint. - accessibilityRole. ``` ### Modern HTML 5 elements ```html
,