Generative UI 无障碍指南:让 AI 界面惠及所有人
让生成式界面对所有用户无障碍访问的实用指南,涵盖屏幕阅读器支持和键盘导航。
为什么 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 增加了额外的挑战:你无法测试每一种可能的组合。
你应该测试的内容:
- 每个组件独立测试。 使用
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();
});
- 常见组合测试。 测试 AI 最可能创建的组合——你的工具调用日志会告诉你哪些组合最常见。
- 键盘导航测试。 手动测试每个组件只用键盘进行交互:Tab、Enter、Space、方向键、Escape。使用 Playwright 进行键盘导航的自动化测试。
- 屏幕阅读器测试。 在 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 - 标题使用多态
headingLevelprop 或 ARIA 标签 - 对比度比率符合 WCAG 2.2 AA(正常文字 4.5:1,大文字 3:1)
-
jest-axe测试通过,零违规 - 人工键盘导航测试已完成
- 屏幕阅读器冒烟测试已完成(VoiceOver 或 NVDA)
构建需要通过无障碍审计的 Generative UI?联系我们——无障碍审计通常能在两到三天内完成,而通过生产 bug 发现同样的问题通常需要几周时间解决。
Alex
Generative UI Engineer & Consultant
专注于 AI 界面与 Generative UI 系统的资深工程师。帮助产品团队用正确的 GenUI 技术栈更快交付。
相关文章
Κατασκευάζοντας το Πρώτο σας Generative UI με το Vercel AI SDK
Βήμα-βήμα οδηγός για τη δημιουργία της πρώτης σας AI-powered διεπαφής με streaming συστατικά.
Προσβασιμότητα σε Generative UI: Δημιουργία Συμπεριληπτικών AI Διεπαφών
Πρακτικός οδηγός για προσβάσιμα γεννητικά interfaces — screen readers, πλοήγηση με πληκτρολόγιο και συνδυαστικά προβλήματα προσβασιμότητας.
CopilotKit vs Vercel AI SDK vs Thesys: Σύγκριση Frameworks
Μια ειλικρινής σύγκριση των τριών κύριων frameworks Generative UI, με πλεονεκτήματα, μειονεκτήματα και πότε να χρησιμοποιείτε το καθένα.
掌握 Generative UI 前沿动态
每周文章、框架更新与实用实现指南——直达你的邮箱。
需要帮助实现你刚读到的内容?
预约免费咨询