#AI#React

AI 产品的可访问性(a11y)

AI 应用的可访问性有两层含义:产品本身要无障碍,以及 AI 能赋能无障碍。本文讲清 AI 产品的 a11y 要点——语义 HTML/键盘/ARIA/动态内容可访问、AI 生成 UI 的 a11y 把控、AI 赋能(图像描述/语音/简化文本),以及一套自动+人工的 a11y 测试流程。

本文要点

  • 主题聚焦:AI、React
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2025年12月18日·更新于 2026年7月23日

AI 产品的可访问性(a11y)有一个常被忽略的双重性:你的产品要对残障用户无障碍,同时你的 AI 能反过来帮残障用户跨过障碍。更关键的是——AI 产品的核心用户群里,辅助技术(屏幕阅读器、开关设备、语音控制)依赖者的比例,显著高于普通 Web 应用。视障用户用 AI 读图、阅读障碍用户用 AI 把长文改简单、听障用户靠 AI 实时转写。这群人正是 a11y 做得好坏的直接受益者或受害者。把 a11y 当配额工程糊弄,等于把最高频、最忠诚的用户挡在门外。

一、AI 应用 a11y 的双重含义

text
1. 产品本身无障碍   —— 你的聊天框、流式输出、生成 UI,屏幕阅读器要能用
2. AI 赋能无障碍    —— 用模型补齐用户的感官/认知缺口:alt text、转写、文本简化

这两层经常被混为一谈,但工程上完全是两件事。前者是前端工程(语义、键盘、ARIA、对比度、焦点),后者是产品能力(把模型当用户的"感官代理")。

还要认清一个事实:AI 本质是"降低门槛"的工具,视障、阅读障碍、老年、非母语用户更愿意用 AI 完成原本困难的事。所以在 AI 产品里,a11y 不是"边缘需求",是核心用户路径上的硬指标。

二、基础 a11y(React 视角)

这部分和普通 Web 一样,但 React/Next.js 里有几个高频翻车点。

1. 语义 HTML 优先,ARIA 是补丁不是替代。 最经典的错:用 <div onClick> 当按钮——Tab 跳不到、Enter 不触发、屏幕阅读器读成"文本"。

tsx
// 反面:<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:

tsx
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 打开时焦点应进到对话框内,关闭后回到触发按钮——这是"焦点陷阱"的基本功:

tsx
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 播报:

tsx
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 审查的组件渲染:
tsx
// 模型输出 { 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=""(空,让屏幕阅读器跳过),信息性图片才填描述,别一刀切全填或全空。
tsx
<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 顺序合理。流式输出时的"停止生成"按钮要能立刻键盘触发,别让用户干等。

tsx
<button aria-label="停止生成" onClick={stop}>
  <StopIcon />
</button>

3. 输入框要有可见 label。 别只放 placeholder 当 label——placeholder 一旦输入就消失,屏幕阅读器读不出字段含义。用 <label htmlFor> 或 aria-label。

4. 历史消息可遍历。 用 <ol>/<li> 结构让语音用户能用阅读键逐条浏览,而不是被当成一大坨连续文本——后者用户根本没法定位"第几条回复"。

六、a11y 测试:自动 + 人工

只靠人工或只靠自动都不够,必须两层叠加。

自动层(进 CI,挡低级错误):

bash
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:
tsx
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

相关推荐