无障碍设计在AI产品中的实践
原创
灵阙教研团队
B 基础 进阶 |
约 10 分钟阅读
更新于 2026-02-28 AI 导读
无障碍设计在AI产品中的实践 WCAG 合规、屏幕阅读器支持与键盘导航的工程实现 1. AI 产品无障碍的特殊挑战 AI 产品在无障碍方面面临传统 Web 应用没有的挑战:动态内容流式生成、非确定性输出、多模态混合、以及对话式交互模式。这些特性使得标准的 WCAG 指南需要额外的解读和扩展。 传统 Web 应用: AI 产品: - 内容预先确定 - 内容实时生成 - 页面结构固定 -...
无障碍设计在AI产品中的实践
WCAG 合规、屏幕阅读器支持与键盘导航的工程实现
1. AI 产品无障碍的特殊挑战
AI 产品在无障碍方面面临传统 Web 应用没有的挑战:动态内容流式生成、非确定性输出、多模态混合、以及对话式交互模式。这些特性使得标准的 WCAG 指南需要额外的解读和扩展。
传统 Web 应用: AI 产品:
- 内容预先确定 - 内容实时生成
- 页面结构固定 - 对话流不断增长
- 交互模式标准化 - 提示词交互非标准
- 错误消息可预测 - AI 可能给出错误/有害内容
- 加载时间短 - 推理时间 5-30 秒
1.1 WCAG 2.1 四原则在 AI 产品中的映射
| 原则 | WCAG 要求 | AI 产品特殊需求 |
|---|---|---|
| 可感知 | 文字替代、字幕、对比度 | 流式文本的实时朗读、代码块的语义化 |
| 可操作 | 键盘、无时限、防癫痫 | 长等待状态的操控、动画安全 |
| 可理解 | 可读、可预测、纠错 | AI 输出的可理解性、置信度传达 |
| 健壮 | 兼容辅助技术 | 动态 DOM 更新不破坏辅助技术 |
2. 屏幕阅读器支持
2.1 对话流的 ARIA 标记
<!-- 对话容器 -->
<div
role="log"
aria-label="与 AI 助手的对话"
aria-live="polite"
aria-relevant="additions"
>
<!-- 用户消息 -->
<article role="article" aria-label="你的消息" class="message message--user">
<div class="message__author" aria-hidden="true">你</div>
<div class="message__content">
请解释量子计算的基本原理。
</div>
<time class="message__time" datetime="2026-02-28T10:30:00">
10:30
</time>
</article>
<!-- AI 回复 -->
<article role="article" aria-label="AI 助手的回复" class="message message--ai">
<div class="message__author" aria-hidden="true">AI 助手</div>
<div class="message__content">
<!-- 流式内容在这里累积 -->
<p>量子计算是一种利用量子力学现象进行计算的技术...</p>
</div>
<footer class="message__meta">
<span class="sr-only">回复完成</span>
<time datetime="2026-02-28T10:30:15">10:30</time>
</footer>
</article>
<!-- 思考状态 -->
<div role="status" aria-live="polite" class="thinking-status">
<span class="sr-only">AI 正在生成回复,请稍候</span>
<div class="thinking-animation" aria-hidden="true">
<!-- 视觉动画 -->
</div>
</div>
</div>
2.2 流式输出的无障碍处理
流式输出对屏幕阅读器是一个挑战:逐字朗读会极度烦人,但完全不通知又让用户无从得知状态。
// 策略: 按段落/句子批量通知,而非逐字
function StreamingAccessibility({ text, isStreaming }) {
const [announcedText, setAnnouncedText] = useState('');
const lastAnnouncedIndex = useRef(0);
useEffect(() => {
if (!isStreaming) {
// 流式结束,通知完整内容
setAnnouncedText('AI 回复完成。');
return;
}
// 检测新句子/段落
const newText = text.slice(lastAnnouncedIndex.current);
const sentenceEnd = newText.match(/[。!?\n]/);
if (sentenceEnd) {
const sentence = newText.slice(0, sentenceEnd.index + 1);
setAnnouncedText(sentence);
lastAnnouncedIndex.current += sentenceEnd.index + 1;
}
}, [text, isStreaming]);
return (
<>
{/* 视觉流式显示 */}
<div aria-hidden="true">{text}</div>
{/* 屏幕阅读器批量通知 */}
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{announcedText}
</div>
</>
);
}
2.3 代码块无障碍
<figure role="group" aria-label="Python 代码示例">
<figcaption class="code-header">
<span class="code-language">Python</span>
<button
class="copy-btn"
aria-label="复制代码到剪贴板"
data-copied-label="已复制"
>
复制
</button>
</figcaption>
<pre role="region" aria-label="代码内容" tabindex="0">
<code class="language-python">
def hello():
print("Hello, World!")
</code>
</pre>
</figure>
3. 键盘导航
3.1 焦点管理策略
Tab 键导航顺序 (AI 聊天界面):
1. 模型选择器
2. 会话列表(侧栏)
3. 对话历史区域
4. 消息操作按钮(复制/赞/踩)
5. 提示词输入框
6. 附件按钮
7. 发送按钮
// 焦点陷阱: Modal 打开时焦点限制在 Modal 内
function useFocusTrap(ref: RefObject<HTMLElement>, active: boolean) {
useEffect(() => {
if (!active || !ref.current) return;
const element = ref.current;
const focusable = element.querySelectorAll(
'a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0] as HTMLElement;
const last = focusable[focusable.length - 1] as HTMLElement;
function handleKeyDown(e: KeyboardEvent) {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}
element.addEventListener('keydown', handleKeyDown);
first?.focus(); // 打开时聚焦第一个元素
return () => element.removeEventListener('keydown', handleKeyDown);
}, [ref, active]);
}
3.2 快捷键设计
| 快捷键 | 功能 | 全局/局部 |
|---|---|---|
| / | 聚焦提示词输入框 | 全局 |
| Ctrl+Enter | 发送消息 | 输入框内 |
| Ctrl+Shift+C | 复制最后一条 AI 回复 | 全局 |
| Ctrl+N | 新建对话 | 全局 |
| Ctrl+K | 打开命令面板 | 全局 |
| Escape | 关闭面板/Modal | 全局 |
| Ctrl+[ / ] | 上/下一个对话 | 全局 |
| Alt+T | 切换侧栏 | 全局 |
// 全局快捷键注册
function useGlobalShortcuts(shortcuts: Record<string, () => void>) {
useEffect(() => {
function handler(e: KeyboardEvent) {
const key = [
e.ctrlKey && 'ctrl',
e.shiftKey && 'shift',
e.altKey && 'alt',
e.key.toLowerCase()
].filter(Boolean).join('+');
if (shortcuts[key]) {
// 不在输入框中时才触发
const target = e.target as HTMLElement;
if (['INPUT', 'TEXTAREA'].includes(target.tagName) && key !== 'escape') {
return;
}
e.preventDefault();
shortcuts[key]();
}
}
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, [shortcuts]);
}
3.3 焦点指示器
/* 清晰的焦点指示器 */
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: 2px;
}
/* 高对比度模式下增强 */
@media (forced-colors: active) {
:focus-visible {
outline: 3px solid Highlight;
outline-offset: 3px;
}
}
/* 焦点指示器不应被其他元素遮挡 */
:focus-visible {
position: relative;
z-index: 1;
}
4. 颜色与对比度
4.1 色彩不作为唯一信息载体
错误做法:
成功: 绿色文字
失败: 红色文字
(色盲用户无法区分)
正确做法:
成功: 绿色文字 + [v] 图标 + "成功" 文字标签
失败: 红色文字 + [x] 图标 + "失败" 文字标签
<!-- 状态指示: 颜色 + 图标 + 文字 -->
<span class="status status--success">
<svg aria-hidden="true" class="status__icon"><!-- 对勾图标 --></svg>
<span>生成成功</span>
</span>
<span class="status status--error">
<svg aria-hidden="true" class="status__icon"><!-- 错误图标 --></svg>
<span>生成失败</span>
</span>
4.2 动态内容的对比度
AI 生成的内容可能包含代码高亮、引用块、表格等,需要确保所有元素的对比度:
function checkDynamicContentContrast(container: HTMLElement) {
const elements = container.querySelectorAll('*');
const issues: ContrastIssue[] = [];
elements.forEach(el => {
const style = getComputedStyle(el);
const color = style.color;
const bgColor = getEffectiveBackground(el);
if (color && bgColor) {
const ratio = calculateContrastRatio(color, bgColor);
const fontSize = parseFloat(style.fontSize);
const isBold = parseInt(style.fontWeight) >= 700;
const isLarge = fontSize >= 18 || (fontSize >= 14 && isBold);
const threshold = isLarge ? 3.0 : 4.5;
if (ratio < threshold) {
issues.push({
element: el,
ratio,
threshold,
color,
bgColor
});
}
}
});
return issues;
}
5. 表单无障碍
5.1 提示词输入框
<div class="prompt-area" role="search" aria-label="AI 对话">
<label for="prompt-input" class="sr-only">
输入您的问题或指令
</label>
<textarea
id="prompt-input"
aria-describedby="prompt-help token-count"
aria-required="true"
placeholder="输入您的问题..."
rows="1"
></textarea>
<div id="prompt-help" class="sr-only">
按 Ctrl+Enter 发送消息。支持 Markdown 格式。
</div>
<div id="token-count" aria-live="polite" class="token-counter">
<span class="sr-only">当前使用</span>
<span>234</span>
<span class="sr-only">个 Token,共</span>
<span>/</span>
<span>4096</span>
<span class="sr-only">个可用</span>
</div>
<button
type="submit"
aria-label="发送消息"
disabled={!hasContent}
>
<svg aria-hidden="true"><!-- 发送图标 --></svg>
</button>
</div>
5.2 模型选择器
<div class="model-selector">
<label id="model-label">选择模型</label>
<div
role="listbox"
aria-labelledby="model-label"
aria-activedescendant="model-sonnet"
tabindex="0"
>
<div
id="model-opus"
role="option"
aria-selected="false"
aria-label="Claude Opus -- 最强能力,适合复杂任务,每次约0.15元"
>
<span class="model-name">Claude Opus</span>
<span class="model-desc">最强能力</span>
</div>
<div
id="model-sonnet"
role="option"
aria-selected="true"
aria-label="Claude Sonnet -- 平衡推荐,适合日常使用,每次约0.03元"
>
<span class="model-name">Claude Sonnet</span>
<span class="model-desc">平衡推荐</span>
</div>
</div>
</div>
6. 动态内容更新
6.1 ARIA Live Region 策略
| 区域 | aria-live 值 | 用途 |
|---|---|---|
| AI 回复区 | polite | 流式内容按句通知 |
| 错误通知 | assertive | 立即中断朗读 |
| Token 计数 | off (手动查询) | 不自动朗读 |
| 状态栏 | polite | 模型切换、连接状态 |
| Toast 通知 | polite | 操作反馈 |
6.2 批量 DOM 更新
// 避免频繁的 aria-live 触发
function useBatchedAnnounce(interval = 500) {
const pending = useRef<string[]>([]);
const announcer = useRef<HTMLDivElement>(null);
useEffect(() => {
const timer = setInterval(() => {
if (pending.current.length > 0 && announcer.current) {
const message = pending.current.join('');
pending.current = [];
announcer.current.textContent = message;
}
}, interval);
return () => clearInterval(timer);
}, [interval]);
function announce(text: string) {
pending.current.push(text);
}
return { announce, announcer };
}
7. 运动与动画安全
7.1 前庭功能障碍防护
/* 系统级减少运动偏好 */
@media (prefers-reduced-motion: reduce) {
/* 禁用所有非必要动画 */
.thinking-animation {
animation: none;
}
.streaming-cursor {
animation: none;
/* 用静态指示替代闪烁 */
border-right: 2px solid var(--color-primary);
}
/* 流式文字直接显示,不逐字出现 */
.stream-char {
animation: none;
opacity: 1;
}
/* 页面过渡瞬间完成 */
.page-transition {
transition-duration: 0.01ms;
}
}
7.2 闪烁检测
// 检测是否有元素闪烁超过 3 次/秒
function checkFlashingContent(element) {
const observer = new MutationObserver((mutations) => {
let visibilityChanges = 0;
const startTime = Date.now();
mutations.forEach(mutation => {
if (mutation.type === 'attributes' &&
(mutation.attributeName === 'class' ||
mutation.attributeName === 'style')) {
visibilityChanges++;
}
});
const elapsed = (Date.now() - startTime) / 1000;
const flashRate = visibilityChanges / elapsed;
if (flashRate > 3) {
console.warn(`[A11y] Element flashing at ${flashRate.toFixed(1)}/s, exceeds 3/s limit`);
}
});
observer.observe(element, { attributes: true, subtree: true });
return observer;
}
8. 测试方法
8.1 自动化测试
// Jest + axe-core 自动化测试
import { axe, toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';
expect.extend(toHaveNoViolations);
describe('ChatInterface Accessibility', () => {
it('should have no accessibility violations', async () => {
const { container } = render(<ChatInterface />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should announce new messages to screen readers', async () => {
const { getByRole } = render(<ChatInterface />);
const log = getByRole('log');
expect(log).toHaveAttribute('aria-live', 'polite');
});
it('should trap focus in modal', async () => {
const { getByRole } = render(<SettingsModal open />);
const modal = getByRole('dialog');
expect(modal).toContainFocusedElement();
});
});
8.2 手动测试检查清单
| 测试项 | 方法 | 通过标准 |
|---|---|---|
| 键盘导航 | 纯键盘操作完整流程 | 所有功能可达 |
| 屏幕阅读器 | VoiceOver / NVDA | 内容正确朗读 |
| 对比度 | axe DevTools | 全部 AA 通过 |
| 缩放 200% | 浏览器缩放到 200% | 无内容丢失 |
| 减少运动 | 系统开启减少运动 | 无动画干扰 |
| 强制颜色 | Windows 高对比度 | 界面可用 |
8.3 屏幕阅读器测试矩阵
| 屏幕阅读器 | 浏览器 | 平台 | 市场份额 |
|---|---|---|---|
| NVDA | Firefox / Chrome | Windows | 40% |
| JAWS | Chrome / Edge | Windows | 30% |
| VoiceOver | Safari | macOS / iOS | 20% |
| TalkBack | Chrome | Android | 8% |
9. WCAG 2.1 AA 合规清单
9.1 AI 产品关键合规项
| WCAG 标准 | 要求 | AI 产品实现 |
|---|---|---|
| 1.1.1 非文本内容 | 所有非文本内容有替代文本 | 图表有 alt text,图标有 aria-label |
| 1.3.1 信息与关系 | 结构可通过程序确定 | 正确使用 heading、list、table 语义 |
| 1.4.3 对比度 | 文字对比度 >= 4.5:1 | Token 系统保证所有主题下满足 |
| 1.4.4 文字缩放 | 200% 缩放不丢失内容 | 响应式布局,rem/em 单位 |
| 2.1.1 键盘 | 所有功能可通过键盘操作 | 完整的键盘导航方案 |
| 2.1.2 无键盘陷阱 | 焦点可以移出任何区域 | 正确的焦点管理 |
| 2.4.7 焦点可见 | 焦点指示器清晰可见 | 2px 实线轮廓 |
| 3.3.1 错误标识 | 自动识别输入错误 | 表单验证 + 错误消息 |
| 4.1.2 名称角色值 | 所有组件有名称和角色 | ARIA 标记完整 |
10. 工具与资源
| 工具 | 用途 | 集成方式 |
|---|---|---|
| axe-core | 自动化检测 | CI / Jest / 浏览器扩展 |
| Lighthouse | 综合审计 | Chrome DevTools / CI |
| WAVE | 可视化检测 | 浏览器扩展 |
| pa11y | CLI 检测 | CI |
| eslint-plugin-jsx-a11y | JSX 无障碍 lint | ESLint |
| @testing-library/react | 无障碍优先测试 | Jest |
| Stark | Figma 无障碍插件 | 设计阶段 |
Maurice | maurice_wen@proton.me