教程

Generative UI 无障碍指南:让 AI 界面惠及所有人

让生成式界面对所有用户无障碍访问的实用指南,涵盖屏幕阅读器支持和键盘导航。

A
Alex11 分钟阅读

为什么 Generative UI 的无障碍更难

你的无障碍团队刚刚审核通过了产品中的每个页面。三周后,AI 生成了一个人类设计师从未画过的布局——这个布局有一个对屏幕阅读器破坏标题层级结构的 heading 层次、一个生成对话框中的焦点陷阱,还有一个颜色是唯一信号的图表。这些都没有在审核中被发现,因为审核时它们还不存在。

这是新的无障碍挑战,而旧的方法论不能覆盖它。

在传统 UI 中,工程师审核每个页面并验证它符合 WCAG 2.2 要求。页面数量是有限的。无障碍团队清楚地知道要测什么。

Generative UI 打破了这个模型。可能的界面集合是无法枚举的——AI 可以以人类从未明确设计过的方式组合组件。一个今天通过无障碍审核的页面,可能在明天与一个新添加的组件组合后产生不可访问的布局。

解决方案是将无障碍要求下沉到组件级别。如果你的库中每个组件都是个别无障碍的,那么它们的任何组合都将是无障碍的——前提是组合本身的结构是正确的。这个限定语意义重大;我们将在"组合无障碍"章节回到这个问题,因为这是大多数生成式 UI 无障碍 bug 实际发生的地方。

这种组件优先的模型比手工审核每个页面更简洁。它也是不可避免的:AI 不会为你添加 ARIA 标签或管理焦点。组件库是你唯一的杠杆点。

组件级别的基准要求

你的生成 UI 工具注册表中的每个组件都必须独立满足以下要求:

语义 HTML 优先。 按钮用 <button>,导航用 <nav>,表格数据用 <table>。当语义元素可用时,不要用 <div onClick={...}>

// 错误:伪装成按钮的 div
<div className="button" onClick={handleClick}>提交</div>

// 正确:真正的 button 元素
<button type="button" onClick={handleClick}>提交</button>

所有图片有 alt 文字。 装饰性图片:alt=""。信息性图片,写一段描述。

颜色不是唯一信号。 一个用绿色显示正值、红色显示负值的图表,需要为无法区分红绿的用户提供另一个指示——+ / - 符号、图标或文字标签。

function TrendIndicator({ value }: { value: number }) {
  const isPositive = value >= 0;
  return (
    <span
      className={isPositive ? 'text-green-600' : 'text-red-600'}
      aria-label={isPositive ? `上涨 ${Math.abs(value)}%` : `下跌 ${Math.abs(value)}%`}
    >
      {/* 图标提供颜色之外的视觉信号 */}
      {isPositive ? '↑' : '↓'} {Math.abs(value)}%
    </span>
  );
}

可交互元素可通过键盘访问。 组件中的每个按钮、链接和表单控件都必须可聚焦且仅用键盘即可操作。

触摸目标足够大。 WCAG 2.2 成功准则 2.5.8(目标大小,最小,AA 级)要求 24×24 CSS 像素;早期的 WCAG 2.1 SC 2.5.5(AAA 级)建议 44×44。移动端主要操作以 AAA 标准为目标——小触摸目标是无障碍问题的主要来源。

流式内容的 ARIA 实时区域

流式传输是 Generative UI 的定义特性——组件随 AI 生成逐步出现。屏幕阅读器不会自动播报动态出现的内容。你必须告诉它们。

使用 aria-live 来播报新生成内容的到来:

// components/genui-output-region.tsx
export function GenUIOutputRegion({ children, isLoading }: {
  children: React.ReactNode;
  isLoading: boolean;
}) {
  return (
    <div
      aria-live="polite"
      aria-busy={isLoading}
      aria-label="AI 生成的内容"
      aria-atomic="false"
    >
      {children}
    </div>
  );
}

这里的关键选择:

  • aria-live="polite" 在下一个空闲时刻播报新内容——不会像 assertive 那样打断用户正在说话。
  • aria-busy={isLoading} 告诉辅助技术该区域正在更新。屏幕阅读器会等到 aria-busy 变为 false 才播报。
  • aria-atomic="false" 在新内容到达时逐个播报,而不是每次重新读取整个区域。

对于加载骨架屏状态:

function LoadingSkeleton({ label }: { label: string }) {
  return (
    <div
      role="status"
      aria-label={`正在加载${label}`}
      className="animate-pulse rounded-lg bg-muted h-32"
    />
  );
}

role="status"aria-live="polite" 区域的隐式形式,用于简短的状态消息。出现时会播报,不打断当前语音。

焦点管理

当生成的内容出现时,键盘焦点会停留在原处。通常这是正确的——你不希望 AI 流式传输组件时焦点到处跳。但对于某些交互,你需要显式移动焦点。

表单提交后替换页面内容时:

const outputRef = useRef<HTMLDivElement>(null);
const [generatedUI, setGeneratedUI] = useState<React.ReactNode>(null);

async function handleSubmit(e: React.FormEvent) {
  e.preventDefault();
  const ui = await generateUI(prompt);
  setGeneratedUI(ui);

  // 将焦点移到输出区域
  // setTimeout 让 React 有时间在我们聚焦之前完成渲染
  setTimeout(() => {
    outputRef.current?.focus();
  }, 0);
}

return (
  <div>
    <form onSubmit={handleSubmit}>...</form>
    <div
      ref={outputRef}
      tabIndex={-1}  // 使 div 可编程聚焦但不在 tab 顺序中
      aria-label="生成的结果"
    >
      {generatedUI}
    </div>
  </div>
);

对话框和模态框: 如果生成的 UI 包含对话框或模态框,焦点必须移到其中并在关闭时返回。使用 <dialog> 元素或完整实现 ARIA 对话框角色。

组合无障碍

这是生成式 UI 无障碍的棘手部分。你可以拥有完全无障碍的个别组件,但仍然产生无障碍的 UI,原因是它们的组合产生了问题。

标题层级。 如果你的 MetricCard 使用 <h3> 作为标题,而 AI 在没有 <h1><h2> 的页面上生成了三个 MetricCard,屏幕阅读器用户的标题导航就会断掉。

解决方案是基于位置而非级别的标题——或者干脆完全避免标题,使用带有语义标签的 ARIA landmark:

// 不好:硬编码标题级别
function MetricCard({ label, value }: MetricCardProps) {
  return (
    <div>
      <h3>{label}</h3>  {/* 如果没有 h1/h2 这会破坏层级 */}
      <span>{value}</span>
    </div>
  );
}

// 好:使用接受级别 prop 的多态标题,或 ARIA 标签
function MetricCard({ label, value, headingLevel = 'h3' }: MetricCardProps & { headingLevel?: 'h2' | 'h3' | 'h4' }) {
  const Heading = headingLevel;
  return (
    <section aria-label={label}>
      <Heading>{label}</Heading>
      <span>{value}</span>
    </section>
  );
}

地标区域。 生成的 UI 可能产生多个 <main> 元素或嵌套的 <nav> 元素。确保你的组件使用 <section><article> 和 ARIA 角色,而不是语义地标元素。

颜色对比度在组合中。 单独看来对比度足够的组件,与 AI 选择的背景颜色配合可能对比度不足。将对比度要求构建到组件的令牌中,使其对背景变化具有鲁棒性。

测试你的组件

传统 UI 测试中的无障碍测试已经有成熟的工具。Generative UI 增加了额外的挑战:你无法测试每一种可能的组合。

你应该测试的内容:

  1. 每个组件独立测试。 使用 jest-axe@axe-core/react 对每个组件运行自动化无障碍测试。这捕获了明显的违规但不是所有的问题。
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

test('MetricCard 没有无障碍违规', async () => {
  const { container } = render(
    <MetricCard label="月营收" value="$12,400" change={5.2} period="vs 上月" />
  );
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
  1. 常见组合测试。 测试 AI 最可能创建的组合——你的工具调用日志会告诉你哪些组合最常见。
  2. 键盘导航测试。 手动测试每个组件只用键盘进行交互:Tab、Enter、Space、方向键、Escape。使用 Playwright 进行键盘导航的自动化测试。
  3. 屏幕阅读器测试。 在 VoiceOver(Mac/iOS)、NVDA(Windows)或 TalkBack(Android)上进行人工测试,重点关注流式传输交互——组件出现时是否被正确播报?

合规参考

WCAG 2.2 AA。 这是欧盟商业服务的基准,自 2025 年 6 月 28 日起欧洲无障碍法案强制要求。中国的无障碍标准 GB/T 37668-2019 与 WCAG 2.0 对齐——如果你在面向中国用户,实现 WCAG 2.2 AA 会同时满足两者。

欧洲无障碍法案(EAA)。 指令 2019/882,于 2025 年 6 月 28 日对商业服务强制执行。Generative UI 库中的每个组件都必须在被模型调用之前通过无障碍审计。

ARIA 规范。 你的骨架屏、实时区域和焦点管理代码应遵循 ARIA 最佳实践指南。不要发明新的 ARIA 角色;使用 APG 中已记录的现有模式。

实践清单

在将每个组件添加到你的生成 UI 注册表之前:

  • 语义 HTML(无 div 伪装成交互元素)
  • 所有图片有描述性 alt 文字
  • 颜色不是唯一的信息信号
  • 所有交互元素可通过键盘访问
  • 触摸目标 ≥24×24px(AA)或 ≥44×44px(AAA)
  • 流式传输的 ARIA 实时区域已设置
  • 骨架屏有 role="status" 和描述性 aria-label
  • 标题使用多态 headingLevel prop 或 ARIA 标签
  • 对比度比率符合 WCAG 2.2 AA(正常文字 4.5:1,大文字 3:1)
  • jest-axe 测试通过,零违规
  • 人工键盘导航测试已完成
  • 屏幕阅读器冒烟测试已完成(VoiceOver 或 NVDA)

构建需要通过无障碍审计的 Generative UI?联系我们——无障碍审计通常能在两到三天内完成,而通过生产 bug 发现同样的问题通常需要几周时间解决。

分享TwitterLinkedIn邮件
accessibilitywcaggenerative-uiinclusive-design
A

Alex

Generative UI Engineer & Consultant

专注于 AI 界面与 Generative UI 系统的资深工程师。帮助产品团队用正确的 GenUI 技术栈更快交付。

掌握 Generative UI 前沿动态

每周文章、框架更新与实用实现指南——直达你的邮箱。

我们尊重你的隐私。随时退订。

需要帮助实现你刚读到的内容?

预约免费咨询