web-accessibility
该技能专注于新UI组件的无障碍设计与开发,并进行无障碍审计,以识别和修复现有系统中的无障碍问题,确保用户体验。
npx skills add supercent-io/skills-template --skill web-accessibilityBefore / After 效果对比
1 组Web 界面未考虑残障用户需求,键盘导航困难,屏幕阅读器无法正确解析内容,导致用户群体受限。
通过遵循 WCAG 标准和最佳实践,设计和实现可访问的组件和表单,确保所有用户都能无障碍地使用产品。
web-accessibility
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: , , , , , etc. Avoid overusing and Use heading hierarchy ( ~ ) correctly Connect with Example (❌ Bad vs ✅ Good): My App Home About My App Home About Form Example: Name: Email: Step 2: Implement Keyboard Navigation Ensure all features are usable without a mouse. Tasks: Move focus with Tab and Shift+Tab Activate buttons with Enter/Space Navigate lists/menus with arrow keys Close modals/dropdowns with ESC Use tabindex appropriately Decision Criteria: Interactive elements → tabindex="0" (focusable) Exclude from focus order → tabindex="-1" (programmatic focus only) Do not change focus order → avoid using tabindex="1+" Example (React Dropdown): import React, { useState, useRef, useEffect } from 'react'; interface DropdownProps { label: string; options: { value: string; label: string }[]; onChange: (value: string) => void; } function AccessibleDropdown({ label, options, onChange }: DropdownProps) { const [isOpen, setIsOpen] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); const buttonRef = useRef(null); const listRef = useRef(null); // Keyboard handler const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'ArrowDown': e.preventDefault(); if (!isOpen) { setIsOpen(true); } else { setSelectedIndex((prev) => (prev + 1) % options.length); } break; case 'ArrowUp': e.preventDefault(); if (!isOpen) { setIsOpen(true); } else { setSelectedIndex((prev) => (prev - 1 + options.length) % options.length); } break; case 'Enter': case ' ': e.preventDefault(); if (isOpen) { onChange(options[selectedIndex].value); setIsOpen(false); buttonRef.current?.focus(); } else { setIsOpen(true); } break; case 'Escape': e.preventDefault(); setIsOpen(false); buttonRef.current?.focus(); break; } }; return ( <button ref={buttonRef} onClick={() => setIsOpen(!isOpen)} onKeyDown={handleKeyDown} aria-haspopup="listbox" aria-expanded={isOpen} aria-labelledby="dropdown-label" > {label} {isOpen && ( {options.map((option, index) => ( <li key={option.value} role="option" aria-selected={index === selectedIndex} onClick={() => { onChange(option.value); setIsOpen(false); }} > {option.label} ))} )} ); } 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: All interactive elements have clear labels Button purpose is clear (e.g., "Submit form" not "Click") State change announcements (aria-live) Decorative images use alt="" or aria-hidden="true" Example (Modal): 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 ( <div role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-description" ref={modalRef} tabIndex={-1} onKeyDown={(e) => { if (e.key === 'Escape') { onClose(); } }} > {title} {children} × ); } aria-live Example (Notifications): function Notification({ message, type }: { message: string; type: 'success' | 'error' }) { return ( <div role="alert" aria-live="assertive" // Immediate announcement (error), "polite" announces in turn aria-atomic="true" // Read the entire content className={notification notification-${type}} > {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): /* ✅ 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): 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( <AccessibleButton onClick={() => {}}> 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 ## Accessibility Checklist ### Semantic HTML - [x] Use semantic HTML tags (<button>, <nav>, <main>, etc.) - [x] Heading hierarchy is correct (h1 → h2 → h3) - [x] All form labels are connected ### Keyboard Navigation - [x] All interactive elements accessible via Tab - [x] Buttons activated with Enter/Space - [x] Modals/dropdowns closed with ESC - [x] Focus indicator is clear (outline) ### ARIA - [x] role used appropriately - [x] aria-label or aria-labelledby provided - [x] aria-live used for dynamic content - [x] Decorative elements use aria-hidden="true" ### Visual - [x] Color contrast meets WCAG AA (4.5:1) - [x] Information not conveyed by color alone - [x] Text size can be adjusted - [x] Responsive design ### Testing - [x] 0 axe DevTools violations - [x] Lighthouse Accessibility score 90+ - [x] Keyboard test passed - [x] Screen reader test completed Constraints Mandatory Rules (MUST) Keyboard Accessibility: All features must be usable without a mouse Support Tab, Enter, Space, arrow keys, and ESC Implement focus trap (for modals) Alternative Text: All images must have an alt attribute Meaningful images: descriptive alt text Decorative images: alt="" (screen reader ignores) Clear Labels: All form inputs must have an associated label or aria-label Do not use placeholder alone as a substitute for a label Prohibited Actions (MUST NOT) Do Not Remove Outline: Never use outline: none Disastrous for keyboard users Must provide a custom focus style instead Do Not Use tabindex > 0: Avoid changing focus order Keep DOM order logical Exception: only when there is a special reason Do Not Convey Information by Color Alone: Accompany with icons or text Consider users with color blindness e.g., "Click red item" → "Click ⚠️ Error item" Examples Example 1: Accessible Form function AccessibleContactForm() { const [errors, setErrors] = useState<Record<string, string>>({}); const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle'); return ( Contact Us Please fill out the form below to get in touch. {/ Name /} Name <input type="text" id="name" name="name" required aria-required="true" aria-invalid={!!errors.name} aria-describedby={errors.name ? 'name-error' : undefined} /> {errors.name && ( {errors.name} )} {/* Email /} Email <input type="email" id="email" name="email" required aria-required="true" aria-invalid={!!errors.email} aria-describedby={errors.email ? 'email-error' : 'email-hint'} /> We'll never share your email. {errors.email && ( {errors.email} )} {/* Submit button /} <button type="submit" disabled={submitStatus === 'loading'}> {submitStatus === 'loading' ? 'Submitting...' : 'Submit'} {/ Success/failure messages /} {submitStatus === 'success' && ( ✅ Form submitted successfully! )} {submitStatus === 'error' && ( ⚠️ An error occurred. Please try again. )} ); } Example 2: Accessible Tab UI 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) => ( <button key={tab.id} role="tab" id={tab-${tab.id}} aria-selected={activeTab === index} aria-controls={panel-${tab.id}} tabIndex={activeTab === index ? 0 : -1} onClick={() => setActiveTab(index)} onKeyDown={(e) => handleKeyDown(e, index)} > {tab.label} ))} {/ Tab Panels */} {tabs.map((tab, index) => ( <div key={tab.id} role="tabpanel" id={panel-${tab.id}} aria-labelledby={tab-${tab.id}} hidden={activeTab !== index} tabIndex={0} > {tab.content} ))} ); } Best practices Semantic HTML First: ARIA is a last resort Using the correct HTML element makes ARIA unnecessary e.g., vs Focus Management: Manage focus on page transitions in SPAs Move focus to main content on new page load Provide skip links ("Skip to main content") Error Messages: Clear and helpful error messages "Invalid input" ❌ → "Email must be in format: example@domain.com" ✅ References WCAG 2.1 Guidelines MDN ARIA WebAIM axe DevTools A11y Project Metadata Version Current Version: 1.0.0 Last Updated: 2025-01-01 Compatible Platforms: Claude, ChatGPT, Gemini Related Skills ui-component-patterns: UI component implementation responsive-design: Responsive design Tags #accessibility #a11y #WCAG #ARIA #screen-reader #keyboard-navigation #frontendWeekly Installs11.6KRepositorysupercent-io/sk…templateGitHub Stars53First SeenJan 24, 2026Security AuditsGen Agent Trust HubPassSocketPassSnykPassInstalled oncodex11.4Kgemini-cli11.4Kopencode11.4Kcursor11.4Kgithub-copilot11.4Kcline11.3K
用户评价 (0)
发表评价
暂无评价
统计数据
用户评分
为此 Skill 评分