🚀 快速安装
复制以下命令并运行,立即安装此 Skill:
npx @anthropic-ai/skills install supercent-io/skills-template/web-accessibility
💡 提示:需要 Node.js 和 NPM
Web 可访问性 (A11y)
何时使用此技能
- 新 UI 组件开发:设计可访问的组件
- 可访问性审计:识别和修复现有站点的可访问性问题
- 表单实现:编写对屏幕阅读器友好的表单
- 模态框/下拉菜单:焦点管理和防止键盘陷阱
- WCAG 合规性:满足法律要求或标准
输入格式
必需信息
- 框架:React、Vue、Svelte、原生 JavaScript 等
- 组件类型:按钮、表单、模态框、下拉菜单、导航等
- WCAG 等级:A、AA、AAA(默认:AA)
可选信息
- 屏幕阅读器:NVDA、JAWS、VoiceOver(用于测试)
- 自动化测试工具:axe-core、Pa11y、Lighthouse(默认:axe-core)
- 浏览器:Chrome、Firefox、Safari(默认:Chrome)
输入示例
创建一个可访问的 React 模态框组件:
- 框架:React + TypeScript
- WCAG 等级:AA
- 要求:
- 焦点陷阱(焦点保持在模态框内)
- 按 ESC 键关闭
- 点击背景关闭
- 标题/描述能被屏幕阅读器读出
指示
步骤 1:使用语义化 HTML
使用有意义的 HTML 元素使结构清晰。
任务:
- 使用语义化标签:
<button>、<nav>、<main>、<header>、<footer>等 - 避免过度使用
<div>和<span> - 正确使用标题层级(
<h1>~<h6>) - 将
<label>与<input>关联
示例(❌ 反面示例 vs ✅ 正确示例):
<!-- ❌ 反面示例:仅使用 div 和 span -->
<div class="header">
<span class="title">我的应用</span>
<div class="nav">
<div class="nav-item" onclick="navigate()">首页</div>
<div class="nav-item" onclick="navigate()">关于</div>
</div>
</div>
<!-- ✅ 正确示例:语义化 HTML -->
<header>
<h1>我的应用</h1>
<nav aria-label="主导航">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/about">关于</a></li>
</ul>
</nav>
</header>
表单示例:
<!-- ❌ 反面示例:没有 label -->
<input type="text" placeholder="输入您的姓名">
<!-- ✅ 正确示例:label 已关联 -->
<label for="name">姓名:</label>
<input type="text" id="name" name="name" required>
<!-- 或者用 label 包裹 -->
<label>
邮箱:
<input type="email" name="email" required>
</label>
步骤 2:实现键盘导航
确保所有功能无需鼠标即可使用。
任务:
- 使用 Tab 和 Shift+Tab 移动焦点
- 使用 Enter/空格键激活按钮
- 使用箭头键导航列表/菜单
- 使用 ESC 键关闭模态框/下拉菜单
- 恰当地使用
tabindex
决策标准:
- 交互式元素 →
tabindex="0"(可获得焦点) - 排除在焦点顺序外 →
tabindex="-1"(只能通过编程方式获得焦点) - 不要更改焦点顺序 → 避免使用
tabindex="1+"
示例(React 下拉菜单):
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<HTMLButtonElement>(null);
const listRef = useRef<HTMLUListElement>(null);
// 键盘处理器
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 (
<div className="dropdown">
<button
ref={buttonRef}
onClick={() => setIsOpen(!isOpen)}
onKeyDown={handleKeyDown}
aria-haspopup="listbox"
aria-expanded={isOpen}
aria-labelledby="dropdown-label"
>
{label}
</button>
{isOpen && (
<ul
ref={listRef}
role="listbox"
aria-labelledby="dropdown-label"
onKeyDown={handleKeyDown}
tabIndex={-1}
>
{options.map((option, index) => (
<li
key={option.value}
role="option"
aria-selected={index === selectedIndex}
onClick={() => {
onChange(option.value);
setIsOpen(false);
}}
>
{option.label}
</li>
))}
</ul>
)}
</div>
);
}
步骤 3:添加 ARIA 属性
为屏幕阅读器提供额外的上下文信息。
任务:
aria-label:定义元素的名称aria-labelledby:引用另一个元素作为标签aria-describedby:提供额外的描述aria-live:宣布动态内容变化aria-hidden:对屏幕阅读器隐藏
检查清单:
- 所有交互式元素都有清晰的标签
- 按钮目的清晰(例如,“提交表单”而不是“点击”)
- 状态变化通告(aria-live)
- 装饰性图片使用 alt=”” 或 aria-hidden=”true”
示例(模态框):
function AccessibleModal({ isOpen, onClose, title, children }) {
const modalRef = useRef<HTMLDivElement>(null);
// 模态框打开时的焦点陷阱
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();
}
}}
>
<div className="modal-overlay" onClick={onClose} aria-hidden="true" />
<div className="modal-content">
<h2 id="modal-title">{title}</h2>
<div id="modal-description">
{children}
</div>
<button onClick={onClose} aria-label="关闭模态框">
<span aria-hidden="true">×</span>
</button>
</div>
</div>
);
}
aria-live 示例(通知):
function Notification({ message, type }: { message: string; type: 'success' | 'error' }) {
return (
<div
role="alert"
aria-live="assertive" // 立即宣布(错误),"polite" 会按序宣布
aria-atomic="true" // 阅读整个内容
className={`notification notification-${type}`}
>
{type === 'error' && <span aria-label="错误">⚠️</span>}
{type === 'success' && <span aria-label="成功">✅</span>}
{message}
</div>
);
}
步骤 4:颜色对比度和视觉可访问性
确保为有视觉障碍的用户提供足够的对比度。
任务:
- WCAG AA:普通文本 4.5:1,大号文本 3:1
- WCAG AAA:普通文本 7:1,大号文本 4.5:1
- 不要仅通过颜色传达信息(同时使用图标、图案)
- 清晰指示焦点(轮廓)
示例(CSS):
/* ✅ 足够的对比度(黑色文本在白色背景上 = 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;
}
步骤 5:可访问性测试
通过自动化和手动测试验证可访问性。
任务:
- 使用 axe DevTools 进行自动化扫描
- 检查 Lighthouse 可访问性得分
- 仅使用键盘测试所有功能
- 屏幕阅读器测试(NVDA、VoiceOver)
示例(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('应无可访问性违规', async () => {
const { container } = render(
<AccessibleButton onClick={() => {}}>
点击我
</AccessibleButton>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('应可通过键盘访问', () => {
const handleClick = jest.fn();
const { getByRole } = render(
<AccessibleButton onClick={handleClick}>
点击我
</AccessibleButton>
);
const button = getByRole('button');
// Enter 键
button.focus();
fireEvent.keyDown(button, { key: 'Enter' });
expect(handleClick).toHaveBeenCalled();
// 空格键
fireEvent.keyDown(button, { key: ' ' });
expect(handleClick).toHaveBeenCalledTimes(2);
});
});
输出格式
基础检查清单
## 可访问性检查清单
### 语义化 HTML
- [x] 使用语义化 HTML 标签(`<button>`、`<nav>`、`<main>` 等)
- [x] 标题层级正确(h1 → h2 → h3)
- [x] 所有表单标签都已关联
### 键盘导航
- [x] 所有交互元素均可通过 Tab 键访问
- [x] 按钮可通过 Enter/空格键激活
- [x] 模态框/下拉菜单可通过 ESC 键关闭
- [x] 焦点指示器清晰可见(轮廓)
### ARIA
- [x] 恰当地使用 `role`
- [x] 提供了 `aria-label` 或 `aria-labelledby`
- [x] 对动态内容使用了 `aria-live`
- [x] 装饰性元素使用了 `aria-hidden="true"`
### 视觉方面
- [x] 颜色对比度符合 WCAG AA 标准(4.5:1)
- [x] 信息不仅通过颜色传达
- [x] 文本大小可以调整
- [x] 响应式设计
### 测试
- [x] 0 个 axe DevTools 违规
- [x] Lighthouse 可访问性得分 90+
- [x] 键盘测试通过
- [x] 屏幕阅读器测试完成
约束条件
必需规则(必须遵守)
- 键盘可访问性:所有功能必须无需鼠标即可使用
- 支持 Tab、Enter、空格、箭头键和 ESC 键
- 实现焦点陷阱(用于模态框)
- 替代文本:所有图片必须包含
alt属性- 有意义的图片:描述性的 alt 文本
- 装饰性图片:
alt=""(屏幕阅读器忽略)
- 清晰的标签:所有表单输入必须有相关联的标签
<label for="...">或aria-label- 不要仅使用占位符替代标签
禁止事项(不得违反)
- 不得移除轮廓:切勿使用
outline: none- 对键盘用户是灾难性的
- 必须提供自定义焦点样式
- 不得使用 tabindex > 0:避免更改焦点顺序
- 保持 DOM 顺序符合逻辑
- 例外:仅在特殊原因时使用
- 不得仅通过颜色传达信息:应配以图标或文本
- 考虑色盲用户
- 例如:“点击红色项” → “点击 ⚠️ 错误项”
示例
示例 1:可访问表单
function AccessibleContactForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
return (
<form onSubmit={handleSubmit} noValidate>
<h2 id="form-title">联系我们</h2>
<p id="form-description">请填写以下表单以便与我们取得联系。</p>
{/* 姓名 */}
<div className="form-group">
<label htmlFor="name">
姓名 <span aria-label="必填">*</span>
</label>
<input
type="text"
id="name"
name="name"
required
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'name-error' : undefined}
/>
{errors.name && (
<span id="name-error" role="alert" className="error">
{errors.name}
</span>
)}
</div>
{/* 邮箱 */}
<div className="form-group">
<label htmlFor="email">
邮箱 <span aria-label="必填">*</span>
</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : 'email-hint'}
/>
<span id="email-hint" className="hint">
我们不会分享您的邮箱。
</span>
{errors.email && (
<span id="email-error" role="alert" className="error">
{errors.email}
</span>
)}
</div>
{/* 提交按钮 */}
<button type="submit" disabled={submitStatus === 'loading'}>
{submitStatus === 'loading' ? '提交中...' : '提交'}
</button>
{/* 成功/失败消息 */}
{submitStatus === 'success' && (
<div role="alert" aria-live="polite" className="success">
✅ 表单提交成功!
</div>
)}
{submitStatus === 'error' && (
<div role="alert" aria-live="assertive" className="error">
⚠️ 发生错误。请重试。
</div>
)}
</form>
);
}
示例 2:可访问标签页 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 (
<div>
{/* 标签页列表 */}
<div role="tablist" aria-label="内容区域">
{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}
</button>
))}
</div>
{/* 标签页面板 */}
{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}
</div>
))}
</div>
);
}
最佳实践
- 语义化 HTML 优先:ARIA 是最后的手段
- 使用正确的 HTML 元素可以使 ARIA 变得不必要
- 例如:
<button>对比<div role="button">
- 焦点管理:在单页应用中管理页面切换时的焦点
- 在新页面加载时将焦点移到主要内容
- 提供跳过链接(“跳转到主要内容”)
- 错误消息:清晰且有用的错误消息
- “无效输入” ❌ → “邮箱格式应为:example@domain.com” ✅
参考资料
元数据
版本
- 当前版本:1.0.0
- 最后更新:2025-01-01
- 兼容平台:Claude, ChatGPT, Gemini
相关技能
- ui-component-patterns:UI 组件实现
- responsive-design:响应式设计
标签
#可访问性 #a11y #WCAG #ARIA #屏幕阅读器 #键盘导航 #前端
📄 原始文档
完整文档(英文):
https://skills.sh/supercent-io/skills-template/web-accessibility
💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

评论(0)