--- name: web-accessibility description: Implement web accessibility (a11y) standards following WCAG 2.1 guidelines. Use when building accessible UIs, fixing accessibility issues, or ensuring compliance with disability standards. Handles ARIA attributes, keyboard navigation, screen readers, semantic HTML, and accessibility testing. tags: [accessibility, a11y, WCAG, ARIA, semantic-HTML, screen-reader] platforms: [Claude, ChatGPT, Gemini] --- # Web Accessibility (A11y) ## When to use this skill - **새 UI 컴포넌트 개발**: 접근 가능한 컴포넌트 설계 - **접근성 감사**: 기존 사이트의 접근성 문제 식별 및 수정 - **폼 구현**: 스크린 리더 친화적인 폼 작성 - **모달/드롭다운**: 포커스 관리 및 키보드 트랩 방지 - **WCAG 준수**: 법적 요구사항 또는 표준 준수 ## 입력 형식 (Input Format) ### 필수 정보 - **프레임워크**: React, Vue, Svelte, Vanilla JS 등 - **컴포넌트 유형**: Button, Form, Modal, Dropdown, Navigation 등 - **WCAG 레벨**: A, AA, AAA (기본값: AA) ### 선택 정보 - **스크린 리더**: NVDA, JAWS, VoiceOver (테스트용) - **자동 테스트 도구**: axe-core, Pa11y, Lighthouse (기본값: axe-core) - **브라우저**: Chrome, Firefox, Safari (기본값: Chrome) ### 입력 예시 ``` React 모달 컴포넌트를 접근 가능하게 만들어줘: - 프레임워크: React + TypeScript - WCAG 레벨: AA - 요구사항: - 포커스 트랩 (모달 내부에만 포커스) - ESC 키로 닫기 - 배경 클릭으로 닫기 - 스크린 리더에서 제목/설명 읽기 ``` ## Instructions ### Step 1: Semantic HTML 사용 의미있는 HTML 요소를 사용하여 구조를 명확히 합니다. **작업 내용**: - ` {isOpen && ( )} ); } ``` ### Step 3: ARIA 속성 추가 스크린 리더에게 추가 컨텍스트를 제공합니다. **작업 내용**: - `aria-label`: 요소의 이름 정의 - `aria-labelledby`: 다른 요소를 라벨로 참조 - `aria-describedby`: 추가 설명 제공 - `aria-live`: 동적 콘텐츠 변경 알림 - `aria-hidden`: 스크린 리더에서 숨기기 **확인 사항**: - [x] 모든 인터랙티브 요소에 명확한 라벨 - [x] 버튼 목적이 명확 (예: "Submit form" not "Click") - [x] 상태 변화 알림 (aria-live) - [x] 장식용 이미지는 alt="" 또는 aria-hidden="true" **예시** (모달): ```tsx function AccessibleModal({ isOpen, onClose, title, children }) { const modalRef = useRef(null); // 모달 열릴 때 포커스 트랩 useEffect(() => { if (isOpen) { modalRef.current?.focus(); } }, [isOpen]); if (!isOpen) return null; return (
{ if (e.key === 'Escape') { onClose(); } }} > ); } ``` **aria-live 예시** (알림): ```tsx function Notification({ message, type }: { message: string; type: 'success' | 'error' }) { return (
{type === 'error' && ⚠️} {type === 'success' && } {message}
); } ``` ### Step 4: 색상 대비 및 시각적 접근성 시각 장애인을 위한 충분한 대비율을 보장합니다. **작업 내용**: - WCAG AA: 텍스트 4.5:1, 큰 텍스트 3:1 - WCAG AAA: 텍스트 7:1, 큰 텍스트 4.5:1 - 색상만으로 정보 전달 금지 (아이콘, 패턴 병행) - 포커스 표시 명확히 (outline) **예시** (CSS): ```css /* ✅ 충분한 대비 (텍스트 #000 on #FFF = 21:1) */ .button { background-color: #0066cc; color: #ffffff; /* 대비율 7.7:1 */ } /* ✅ 포커스 표시 */ button:focus, a:focus { outline: 3px solid #0066cc; outline-offset: 2px; } /* ❌ outline: none 금지! */ button:focus { outline: none; /* 절대 사용 금지 */ } /* ✅ 색상 + 아이콘으로 상태 표시 */ .error-message { color: #d32f2f; border-left: 4px solid #d32f2f; } .error-message::before { content: '⚠️'; margin-right: 8px; } ``` ### Step 5: 접근성 테스트 자동 및 수동 테스트로 접근성을 검증합니다. **작업 내용**: - axe DevTools로 자동 스캔 - Lighthouse Accessibility 점수 확인 - 키보드만으로 전체 기능 테스트 - 스크린 리더 테스트 (NVDA, VoiceOver) **예시** (Jest + axe-core): ```typescript import { render } from '@testing-library/react'; import { axe, toHaveNoViolations } from 'jest-axe'; import AccessibleButton from './AccessibleButton'; expect.extend(toHaveNoViolations); describe('AccessibleButton', () => { it('should have no accessibility violations', async () => { const { container } = render( {}}> Click Me ); const results = await axe(container); expect(results).toHaveNoViolations(); }); it('should be keyboard accessible', () => { const handleClick = jest.fn(); const { getByRole } = render( Click Me ); const button = getByRole('button'); // Enter 키 button.focus(); fireEvent.keyDown(button, { key: 'Enter' }); expect(handleClick).toHaveBeenCalled(); // Space 키 fireEvent.keyDown(button, { key: ' ' }); expect(handleClick).toHaveBeenCalledTimes(2); }); }); ``` ## Output format ### 기본 체크리스트 ```markdown ## Accessibility Checklist ### Semantic HTML - [x] 시맨틱 HTML 태그 사용 (` {/* 성공/실패 메시지 */} {submitStatus === 'success' && (
✅ Form submitted successfully!
)} {submitStatus === 'error' && (
⚠️ An error occurred. Please try again.
)} ); } ``` ### 예시 2: 접근 가능한 탭 UI ```tsx function AccessibleTabs({ tabs }: { tabs: { id: string; label: string; content: React.ReactNode }[] }) { const [activeTab, setActiveTab] = useState(0); const handleKeyDown = (e: React.KeyboardEvent, index: number) => { switch (e.key) { case 'ArrowRight': e.preventDefault(); setActiveTab((index + 1) % tabs.length); break; case 'ArrowLeft': e.preventDefault(); setActiveTab((index - 1 + tabs.length) % tabs.length); break; case 'Home': e.preventDefault(); setActiveTab(0); break; case 'End': e.preventDefault(); setActiveTab(tabs.length - 1); break; } }; return (
{/* Tab List */}
{tabs.map((tab, index) => ( ))}
{/* Tab Panels */} {tabs.map((tab, index) => ( ))}
); } ``` ## Best practices 1. **시맨틱 HTML 우선**: ARIA는 마지막 수단 - 올바른 HTML 요소 사용하면 ARIA 불필요 - 예: `