AI 产品的可访问性(a11y)
AI 应用的可访问性有两层含义:产品本身要无障碍,以及 AI 能赋能无障碍。本文讲清 AI 产品的 a11y 要点——语义 HTML/键盘/ARIA/动态内容可访问、AI 生成 UI 的 a11y 把控、AI 赋能(图像描述/语音/简化文本),以及一套自动+人工的 a11y 测试流程。
本文要点
- 主题聚焦:AI、React
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
AI 产品的可访问性(a11y)有一个常被忽略的双重性:你的产品要对残障用户无障碍,同时你的 AI 能反过来帮残障用户跨过障碍。更关键的是——AI 产品的核心用户群里,辅助技术(屏幕阅读器、开关设备、语音控制)依赖者的比例,显著高于普通 Web 应用。视障用户用 AI 读图、阅读障碍用户用 AI 把长文改简单、听障用户靠 AI 实时转写。这群人正是 a11y 做得好坏的直接受益者或受害者。把 a11y 当配额工程糊弄,等于把最高频、最忠诚的用户挡在门外。
一、AI 应用 a11y 的双重含义
1. 产品本身无障碍 —— 你的聊天框、流式输出、生成 UI,屏幕阅读器要能用
2. AI 赋能无障碍 —— 用模型补齐用户的感官/认知缺口:alt text、转写、文本简化
这两层经常被混为一谈,但工程上完全是两件事。前者是前端工程(语义、键盘、ARIA、对比度、焦点),后者是产品能力(把模型当用户的"感官代理")。
还要认清一个事实:AI 本质是"降低门槛"的工具,视障、阅读障碍、老年、非母语用户更愿意用 AI 完成原本困难的事。所以在 AI 产品里,a11y 不是"边缘需求",是核心用户路径上的硬指标。
二、基础 a11y(React 视角)
这部分和普通 Web 一样,但 React/Next.js 里有几个高频翻车点。
1. 语义 HTML 优先,ARIA 是补丁不是替代。 最经典的错:用 <div onClick> 当按钮——Tab 跳不到、Enter 不触发、屏幕阅读器读成"文本"。
// 反面:<div onClick={handleSubmit}>提交</div>
// 正面:
<button onClick={handleSubmit}>提交</button>
一条原则:能用原生语义标签就别上 ARIA。<button>、<nav>、<main>、<dialog>、<a> 自带语义和键盘行为,而一个 role="button" 的 <div> 永远是劣选——你要手写 tabIndex、onKeyDown、焦点环,漏一个就是 bug。
2. 键盘可达 + 焦点可见。 所有交互元素必须能 Tab 到、能操作。别用 outline: none 把焦点环去掉又不补样式——这是 a11y 里最廉价的扣分项。自定义可交互组件要管 tabIndex 和 onKeyDown:
function Chip({ onSelect, label }: { onSelect: () => void; label: string }) {
return (
<span
role="button"
tabIndex={0}
onClick={onSelect}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onSelect();
}
}}
className="focus-visible:outline-2 focus-visible:outline-blue-500"
>
{label}
</span>
);
}
用 focus-visible 而不是 focus,鼠标点击时不显示焦点环、键盘操作时才显示,体验更干净。
3. 焦点管理。 路由切换、Modal 打开/关闭、动态插入内容时,焦点别丢。Modal 打开时焦点应进到对话框内,关闭后回到触发按钮——这是"焦点陷阱"的基本功:
function Modal({ open, onClose, title, children }: Props) {
const panelRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (open) panelRef.current?.focus();
else triggerRef.current?.focus(); // 关闭后焦点归位到触发按钮
}, [open]);
if (!open) return null;
return (
<div role="dialog" aria-modal="true" aria-labelledby="modal-title"
tabIndex={-1} ref={panelRef} className="outline-none">
<h2 id="modal-title">{title}</h2>
{children}
</div>
);
}
tabIndex={-1} 让容器可编程聚焦但不进 Tab 序列,这是 Modal 焦点管理的标准手法。
4. 对比度 + 文字缩放。 WCAG AA 要求正文 4.5:1、大字 3:1。AI 产品最爱踩这条的地方:灰色辅助文字(token 计数、时间戳、"复制"按钮的提示)。字号用 rem 不用 px,让浏览器缩放和用户偏好生效。
5. ARIA 别滥用。 aria-label 不是万金油——给已经有可见文字的按钮加 aria-label 会覆盖原文字,反而打断语音用户的心智模型。原则:可见文字优先,语义不足才上 ARIA。
三、AI 应用特有的 a11y:动态内容
普通页面的 DOM 静态稳定,屏幕阅读器读完就停。AI 应用不一样——内容会动态长出来,这带来两个独有的难题。
1. 流式输出对屏幕阅读器友好吗?不友好。 如果你把流式 token 直接塞进一个 aria-live 区域,屏幕阅读器会每收到一个增量就读一次——用户听到的是碎片化的"你""好""我""是",完全没法用。这是 AI 应用最典型的 a11y 翻车。
正确做法:别让流式文本进 live region。流式内容正常渲染给视觉用户,关键状态用单独的、视觉隐藏的 live region 播报:
function AssistantMessage({
content,
status,
}: {
content: string;
status: "streaming" | "done" | "error";
}) {
return (
<>
{/* 流式文本正常渲染,不是 live region */}
<div className="prose">{content}</div>
{/* 视觉隐藏的 live region,只播报"状态变化" */}
<div aria-live="polite" className="sr-only" aria-busy={status === "streaming"}>
{status === "streaming" && "AI 正在回复"}
{status === "done" && "回复完成"}
{status === "error" && "回复出错"}
</div>
</>
);
}
视觉用户看到流式打字、语音用户听到一次"AI 正在回复",结束后用方向键跳到完整消息自己读。两边都顺。
2. AI 生成 UI 的可访问性把控。 很多 AI 产品让模型直接生成 UI(表格、表单、卡片)。模型可不管语义 HTML——它会给你 <div> 套 <div>、onclick 写死、<input> 没 label。把控手段只有一条:模型产出数据,语义由你的组件保证。
- 别让模型直接吐 HTML 渲染。 让模型输出结构化数据(JSON schema),前端用受控的、已过 a11y 审查的组件渲染:
// 模型输出 { type: "table", columns: [...], rows: [...] },不是 <table> 字符串
function GeneratedUI({ spec }: { spec: UISpec }) {
switch (spec.type) {
case "table":
return <DataTable columns={spec.columns} rows={spec.rows} />; // 组件库已保证 a11y
case "form":
return <SchemaForm fields={spec.fields} />; // 每个 field 自动绑 label
}
}
- 若必须渲染模型生成的 markdown/HTML,过白名单 sanitizer(允许语义标签、剥离
onclick/style/javascript:),再用 jest-axe 跑一遍 a11y 断言再放行。一句话:永远不要dangerouslySetInnerHTML裸渲模型产出。
四、AI 赋能无障碍
这是 AI 产品独有的优势——模型反过来当用户的"感官代理",把传统软件做不到的事变成产品能力:
- 自动 alt text。图片上传时调多模态模型生成描述填进
alt。注意区分:装饰性图片alt=""(空,让屏幕阅读器跳过),信息性图片才填描述,别一刀切全填或全空。
<img src={img.url} alt={img.informational ? (img.aiDescription ?? "") : ""} />
-
语音转写 + 字幕。音频/视频内容用 Whisper 类模型转写,生成 WebVTT 字幕挂到
<track>,实时场景做流式转写。这是听障用户的核心入口,也是 AI 产品该默认提供的能力。 -
文本简化(面向认知/阅读障碍)。长文、专业文提供"简化版本"按钮,让模型重写成更短句、更通俗的表达。面向低识字率、老年、非母语用户同样有效。prompt 约束:"用 A2/B1 级词汇,短句,避免术语,保留关键信息"。
-
实时字幕/手语翻译。视频通话、直播场景,流式转写叠加手语虚拟人,是 AI 在 a11y 的前沿落地。
判断标准很直接:凡是模型能补齐的感官或认知缺口,都该作为产品能力提供——这恰恰是 AI 产品区别于传统软件的差异化,也是 a11y 的最高阶形态。
五、聊天/对话界面的 a11y
聊天是 AI 产品的核心交互,a11y 坑也最集中。四个要点:
1. aria-live 的正确分级。
- 历史消息列表:不要做成 live region。它是静态可遍历内容,做成普通列表(
<ol>/<li>或role="log"),让语音用户用上下方向键自己读。 - 状态播报(生成中/完成/出错):用单独的
aria-live="polite"区域,见第三节。 - 紧急错误(如内容审核拦截):
aria-live="assertive",但少用——它会打断用户当前朗读。
2. 按钮可达。 发送、停止生成、复制、重试——都必须是原生 <button>,图标按钮尤其要给 aria-label,Tab 顺序合理。流式输出时的"停止生成"按钮要能立刻键盘触发,别让用户干等。
<button aria-label="停止生成" onClick={stop}>
<StopIcon />
</button>
3. 输入框要有可见 label。 别只放 placeholder 当 label——placeholder 一旦输入就消失,屏幕阅读器读不出字段含义。用 <label htmlFor> 或 aria-label。
4. 历史消息可遍历。 用 <ol>/<li> 结构让语音用户能用阅读键逐条浏览,而不是被当成一大坨连续文本——后者用户根本没法定位"第几条回复"。
六、a11y 测试:自动 + 人工
只靠人工或只靠自动都不够,必须两层叠加。
自动层(进 CI,挡低级错误):
pnpm add -D eslint-plugin-jsx-a11y @axe-core/playwright jest-axe
- eslint-plugin-jsx-a11y:lint 期挡
alt缺失、onclick无onkeydown、tabindex滥用等。零成本,必装。 - axe-core(jest-axe / @axe-core/playwright):跑真实渲染的 DOM,断言 WCAG 违规数为 0:
import { axe } from "jest-axe";
it("Chat 页面无 a11y 违规", async () => {
const { container } = render(<Chat />);
expect(await axe(container)).toHaveNoViolations();
});
- Lighthouse a11y:CI 卡分(如 ≥ 95),但记住 Lighthouse 只覆盖约三四十项,高分绝不等于真的可用——它能挡低级错,挡不住"流式输出把屏幕阅读器搞炸"这种交互级问题。
人工层(自动测不出,只能人测):
- 纯键盘走查:拔掉鼠标,Tab/Enter/Esc/方向键走完核心流程,看焦点是否可见、能否退出 Modal、能否停止生成、能否复制。这一步能挡掉一大半真实障碍,成本极低。
- 屏幕阅读器实测:Windows 用 NVDA(免费),Mac 用 VoiceOver(内置),移动端 TalkBack/VoiceOver。真实跑一遍:流式状态能听懂吗?历史消息能遍历吗?错误提示能听到吗?
- 减动效偏好:尊重
prefers-reduced-motion,流式打字机效果在该偏好下应关闭或减弱——前庭障碍用户会被动画诱发眩晕。
自动挡 80% 的低级错,人工抓剩下 20% 的真实体验问题。没有人工实测的 a11y,本质没做完。
七、把 a11y 当一等公民
a11y 失败几乎都源于"事后补"——上线前找个人过一遍、修修对比度。这种修法只能补表面,补不进交互流程,更补不进生成 UI 的语义把控。
正确姿势:
- 设计阶段就把焦点顺序、流式播报策略、键盘交互画进原型,而不是等开发完再问"这能不能 Tab"。
- 组件库内置 a11y。选 Radix UI / shadcn 这类"无样式、自带 a11y"的底座,从源头杜绝
<div onClick>满天飞。自己造的组件,DoD 里写明必须过 axe 断言。 - a11y 进 DoD(Definition of Done)。每个 PR 必须过 eslint-plugin-jsx-a11y + axe 断言,核心流程附键盘走查说明。
- 把它当功能指标,不是"加分项"。它和合规风险(欧盟 EAA 2025 年生效、美国 ADA 诉讼频发)、用户增长直接挂钩——不是慈善,是生意。
小结
AI 产品的 a11y 有两层,都不能漏:
- 基础 a11y:语义 HTML、键盘可达、焦点管理、对比度、ARIA 克制——和普通 Web 一样的硬功。
- 动态内容 a11y:流式文本别进 live region,用独立的视觉隐藏区域播报状态;模型生成的 UI 走结构化数据 + 受控组件,绝不裸渲 HTML。
- AI 赋能无障碍:自动 alt text、转写字幕、文本简化——把模型当用户的感官代理,这是 AI 产品独有的差异化。
- 测试两层:自动(jsx-a11y + axe + Lighthouse)挡低级错,人工(纯键盘 + NVDA/VoiceOver)抓真实体验,缺一不可。
- 一等公民:设计、组件库、DoD 全链路内置,不是上线前补丁。
记住一句:a11y 不是"对少数用户的善意",而是 AI 产品的基本工程质量——尤其当你的核心用户里就有大量依赖辅助技术的人,把流式输出对屏幕阅读器友好做对、把生成 UI 的语义把住,比多加一个酷炫功能重要得多。
Related