--- 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. metadata: tags: accessibility, a11y, WCAG, ARIA, semantic-HTML, screen-reader platforms: Claude, ChatGPT, Gemini --- # Web Accessibility (A11y) ## When to use this skill - **New UI Component Development**: Designing accessible components - **Accessibility Audit**: Identifying and fixing accessibility issues in existing sites - **Form Implementation**: Writing screen reader-friendly forms - **Modals/Dropdowns**: Focus management and keyboard trap prevention - **WCAG Compliance**: Meeting legal requirements or standards ## Input Format ### Required Information - **Framework**: React, Vue, Svelte, Vanilla JS, etc. - **Component Type**: Button, Form, Modal, Dropdown, Navigation, etc. - **WCAG Level**: A, AA, AAA (default: AA) ### Optional Information - **Screen Reader**: NVDA, JAWS, VoiceOver (for testing) - **Automated Testing Tool**: axe-core, Pa11y, Lighthouse (default: axe-core) - **Browser**: Chrome, Firefox, Safari (default: Chrome) ### Input Example ``` Make a React modal component accessible: - Framework: React + TypeScript - WCAG Level: AA - Requirements: - Focus trap (focus stays inside the modal) - Close with ESC key - Close by clicking the background - Title/description read by screen readers ``` ## Instructions ### Step 1: Use Semantic HTML Use meaningful HTML elements to make the structure clear. **Tasks**: - Use semantic tags: ` {isOpen && ( )} ); } ``` ### Step 3: Add ARIA Attributes Provide additional context for screen readers. **Tasks**: - `aria-label`: Define the element's name - `aria-labelledby`: Reference another element as a label - `aria-describedby`: Provide additional description - `aria-live`: Announce dynamic content changes - `aria-hidden`: Hide from screen readers **Checklist**: - [x] All interactive elements have clear labels - [x] Button purpose is clear (e.g., "Submit form" not "Click") - [x] State change announcements (aria-live) - [x] Decorative images use alt="" or aria-hidden="true" **Example** (Modal): ```tsx function AccessibleModal({ isOpen, onClose, title, children }) { const modalRef = useRef(null); // Focus trap when modal opens useEffect(() => { if (isOpen) { modalRef.current?.focus(); } }, [isOpen]); if (!isOpen) return null; return (
{ if (e.key === 'Escape') { onClose(); } }} > ); } ``` **aria-live Example** (Notifications): ```tsx function Notification({ message, type }: { message: string; type: 'success' | 'error' }) { return (
{type === 'error' && ⚠️} {type === 'success' && } {message}
); } ``` ### Step 4: Color Contrast and Visual Accessibility Ensure sufficient contrast ratios for users with visual impairments. **Tasks**: - WCAG AA: text 4.5:1, large text 3:1 - WCAG AAA: text 7:1, large text 4.5:1 - Do not convey information by color alone (use icons, patterns alongside) - Clearly indicate focus (outline) **Example** (CSS): ```css /* ✅ Sufficient contrast (text #000 on #FFF = 21:1) */ .button { background-color: #0066cc; color: #ffffff; /* contrast ratio 7.7:1 */ } /* ✅ Focus indicator */ button:focus, a:focus { outline: 3px solid #0066cc; outline-offset: 2px; } /* ❌ outline: none is forbidden! */ button:focus { outline: none; /* Never use this */ } /* ✅ Indicate state with color + icon */ .error-message { color: #d32f2f; border-left: 4px solid #d32f2f; } .error-message::before { content: '⚠️'; margin-right: 8px; } ``` ### Step 5: Accessibility Testing Validate accessibility with automated and manual testing. **Tasks**: - Automated scan with axe DevTools - Check Lighthouse Accessibility score - Test all features with keyboard only - Screen reader testing (NVDA, VoiceOver) **Example** (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 key button.focus(); fireEvent.keyDown(button, { key: 'Enter' }); expect(handleClick).toHaveBeenCalled(); // Space key fireEvent.keyDown(button, { key: ' ' }); expect(handleClick).toHaveBeenCalledTimes(2); }); }); ``` ## Output format ### Basic Checklist ```markdown ## Accessibility Checklist ### Semantic HTML - [x] Use semantic HTML tags (` {/* Success/failure messages */} {submitStatus === 'success' && (
✅ Form submitted successfully!
)} {submitStatus === 'error' && (
⚠️ An error occurred. Please try again.
)} ); } ``` ### Example 2: Accessible Tab 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. **Semantic HTML First**: ARIA is a last resort - Using the correct HTML element makes ARIA unnecessary - e.g., `