🚀 快速安装
复制以下命令并运行,立即安装此 Skill:
npx @anthropic-ai/skills install wshobson/agents/accessibility-compliance
💡 提示:需要 Node.js 和 NPM
无障碍合规
掌握无障碍实现,为包括残障人士在内的所有人创造包容的体验。
何时使用此技能
- 实现 WCAG 2.2 AA 级或 AAA 级合规
- 构建屏幕阅读器可访问的界面
- 为交互式组件添加键盘导航
- 实现焦点管理和焦点陷阱
- 创建具有正确标签的无障碍表单
- 支持减少动效和高对比度偏好
- 构建移动端无障碍功能(iOS VoiceOver,Android TalkBack)
- 进行无障碍审计并修复违规问题
核心能力
1. WCAG 2.2 指南
- 可感知:内容必须以不同的方式呈现
- 可操作:界面必须能通过键盘和辅助技术导航
- 可理解:内容和操作必须清晰
- 稳健:内容必须能与当前及未来的辅助技术配合使用
2. ARIA 模式
- 角色:定义元素用途(按钮、对话框、导航)
- 状态:指示当前状况(展开、选中、禁用)
- 属性:描述关系和附加信息(由…标记、由…描述)
- 活动区域:宣布动态内容变化
3. 键盘导航
- 焦点顺序和 Tab 键顺序
- 焦点指示器和可见焦点状态
- 键盘快捷键
- 模态框和对话框的焦点陷阱
4. 屏幕阅读器支持
- 语义化 HTML 结构
- 图片的替代文本
- 正确的标题层级
- 跳转链接和地标
5. 移动端无障碍
- 触摸目标尺寸(最小 44x44dp)
- 与 VoiceOver 和 TalkBack 的兼容性
- 手势替代方案
- 动态类型支持
快速参考
WCAG 2.2 成功标准清单
| 级别 | 标准 | 描述 |
|---|---|---|
| A | 1.1.1 | 非文本内容有文本替代方案 |
| A | 1.3.1 | 信息和关系可通过编程方式确定 |
| A | 2.1.1 | 所有功能可通过键盘访问 |
| A | 2.4.1 | 提供跳过主要内容块的机制 |
| AA | 1.4.3 | 对比度至少 4.5:1(文本),3:1(大文本) |
| AA | 1.4.11 | 非文本对比度至少 3:1 |
| AA | 2.4.7 | 焦点可见 |
| AA | 2.5.8 | 目标尺寸最小 24x24px(2.2 版新增) |
| AAA | 1.4.6 | 增强对比度 7:1 |
| AAA | 2.5.5 | 目标尺寸最小 44x44px |
关键模式
模式 1:无障碍按钮
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary";
isLoading?: boolean;
}
function AccessibleButton({
children,
variant = "primary",
isLoading = false,
disabled,
...props
}: ButtonProps) {
return (
<button
// 加载时禁用
disabled={disabled || isLoading}
// 向屏幕阅读器宣布加载状态
aria-busy={isLoading}
// 描述按钮的当前状态
aria-disabled={disabled || isLoading}
className={cn(
// 可见焦点环
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
// 最小触摸目标尺寸 (44x44px)
"min-h-[44px] min-w-[44px]",
variant === "primary" && "bg-primary text-primary-foreground",
(disabled || isLoading) && "opacity-50 cursor-not-allowed",
)}
{...props}
>
{isLoading ? (
<>
<span className="sr-only">加载中</span>
<Spinner aria-hidden="true" />
</>
) : (
children
)}
</button>
);
}
模式 2:无障碍模态对话框
import * as React from "react";
import { FocusTrap } from "@headlessui/react";
interface DialogProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
function AccessibleDialog({ isOpen, onClose, title, children }: DialogProps) {
const titleId = React.useId();
const descriptionId = React.useId();
// 按 Escape 键关闭
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isOpen) {
onClose();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose]);
// 打开时防止页面滚动
React.useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
}
return () => {
document.body.style.overflow = "";
};
}, [isOpen]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={descriptionId}
>
{/* 背景遮罩 */}
<div
className="fixed inset-0 bg-black/50"
aria-hidden="true"
onClick={onClose}
/>
{/* 焦点陷阱容器 */}
<FocusTrap>
<div className="fixed inset-0 flex items-center justify-center p-4">
<div className="bg-background rounded-lg shadow-lg max-w-md w-full p-6">
<h2 id={titleId} className="text-lg font-semibold">
{title}
</h2>
<div id={descriptionId}>{children}</div>
<button
onClick={onClose}
className="absolute top-4 right-4"
aria-label="关闭对话框"
>
<X className="h-4 w-4" />
</button>
</div>
</div>
</FocusTrap>
</div>
);
}
模式 3:无障碍表单
function AccessibleForm() {
const [errors, setErrors] = React.useState<Record<string, string>>({});
return (
<form aria-describedby="form-errors" noValidate>
{/* 供屏幕阅读器使用的错误摘要 */}
{Object.keys(errors).length > 0 && (
<div
id="form-errors"
role="alert"
aria-live="assertive"
className="bg-destructive/10 border border-destructive p-4 rounded-md mb-4"
>
<h2 className="font-semibold text-destructive">
请修复以下错误:
</h2>
<ul className="list-disc list-inside mt-2">
{Object.entries(errors).map(([field, message]) => (
<li key={field}>
<a href={`#${field}`} className="underline">
{message}
</a>
</li>
))}
</ul>
</div>
)}
{/* 带有错误的必填字段 */}
<div className="space-y-2">
<label htmlFor="email" className="block font-medium">
邮箱地址
<span aria-hidden="true" className="text-destructive ml-1">
*
</span>
<span className="sr-only">(必填)</span>
</label>
<input
id="email"
name="email"
type="email"
required
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? "email-error" : "email-hint"}
className={cn(
"w-full px-3 py-2 border rounded-md",
errors.email && "border-destructive",
)}
/>
{errors.email ? (
<p id="email-error" className="text-sm text-destructive" role="alert">
{errors.email}
</p>
) : (
<p id="email-hint" className="text-sm text-muted-foreground">
我们绝不会分享您的邮箱。
</p>
)}
</div>
<button type="submit" className="mt-4">
提交
</button>
</form>
);
}
模式 4:跳过导航链接
function SkipLink() {
return (
<a
href="#main-content"
className={cn(
// 默认隐藏,获得焦点时可见
"sr-only focus:not-sr-only",
"focus:absolute focus:top-4 focus:left-4 focus:z-50",
"focus:bg-background focus:px-4 focus:py-2 focus:rounded-md",
"focus:ring-2 focus:ring-primary",
)}
>
跳转到主要内容
</a>
);
}
// 在布局中使用
function Layout({ children }) {
return (
<>
<SkipLink />
<header>...</header>
<nav aria-label="主导航">...</nav>
<main id="main-content" tabIndex={-1}>
{children}
</main>
<footer>...</footer>
</>
);
}
模式 5:用于宣布信息的活动区域
function useAnnounce() {
const [message, setMessage] = React.useState("");
const announce = React.useCallback(
(text: string, priority: "polite" | "assertive" = "polite") => {
setMessage(""); // 先清除以确保重新宣布
setTimeout(() => setMessage(text), 100);
},
[],
);
const Announcer = () => (
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{message}
</div>
);
return { announce, Announcer };
}
// 使用示例
function SearchResults({ results, isLoading }) {
const { announce, Announcer } = useAnnounce();
React.useEffect(() => {
if (!isLoading && results) {
announce(`${results.length} 个结果已找到`);
}
}, [results, isLoading, announce]);
return (
<>
<Announcer />
<ul>{/* 结果列表 */}</ul>
</>
);
}
颜色对比度要求
// 对比度工具函数
function getContrastRatio(foreground: string, background: string): number {
const fgLuminance = getLuminance(foreground);
const bgLuminance = getLuminance(background);
const lighter = Math.max(fgLuminance, bgLuminance);
const darker = Math.min(fgLuminance, bgLuminance);
return (lighter + 0.05) / (darker + 0.05);
}
// WCAG 要求
const CONTRAST_REQUIREMENTS = {
// 普通文本 (<18pt 或 <14pt 粗体)
normalText: {
AA: 4.5,
AAA: 7,
},
// 大文本 (>=18pt 或 >=14pt 粗体)
largeText: {
AA: 3,
AAA: 4.5,
},
// UI 组件和图形
uiComponents: {
AA: 3,
},
};
最佳实践
- 使用语义化 HTML:尽可能优先使用原生元素而非 ARIA
- 让真实用户测试:在用户测试中包括残障人士
- 键盘优先:设计无需鼠标即可工作的交互
- 不要禁用焦点样式:样式化它们,而不是移除它们
- 提供文本替代方案:所有非文本内容都需要描述
- 支持缩放:内容应在 200% 缩放比例下正常工作
- 宣布变化:对动态内容使用活动区域
- 尊重用户偏好:遵守减少动效和高对比度偏好设置
常见问题
- 缺少替代文本:没有描述的图片
- 颜色对比度差:文本在背景上难以阅读
- 键盘陷阱:焦点困在组件中
- 缺少标签:没有关联标签的表单输入
- 自动播放媒体:未经用户启动就播放的内容
- 不可访问的自定义控件:重新实现原生功能但效果不佳
- 缺少跳转链接:无法绕过重复内容
- 焦点顺序问题:Tab 键顺序与视觉顺序不符
测试工具
- 自动化:axe DevTools,WAVE,Lighthouse
- 手动:VoiceOver (macOS/iOS),NVDA/JAWS (Windows),TalkBack (Android)
- 模拟器:NoCoffee(视觉),Silktide(各种残障)
📄 原始文档
完整文档(英文):
https://skills.sh/wshobson/agents/accessibility-compliance
💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

评论(0)