无障碍组件库调研
哪些真正能通过axe审计
我们安装了2026年下载量最高的七款React组件库,将每一款接入全新的Next.js 15 / React 19 / TypeScript应用,并对每个原生组件运行了同一套审计工具链:无头Chromium中的axe-core 4.11、人工键盘测试、Windows平台NVDA和macOS平台VoiceOver,以及Lighthouse无障碍分项的包体积成本评估。
1. 审计工具链
所有库均安装至同一个React 19 / Next.js 15 / TypeScript 5.5脚手架,按各供应商当前推荐的安装方式进行:打包库(Radix UI primitives、Headless UI 2.x、Mantine 8.x、Chakra UI v3、Ark UI、React Aria Components)使用npm i安装,shadcn/ui通过shadcn CLI安装——该方式将源代码复制到components/ui,而非作为包引入。随后我们挂载了一个全量演示页面,以默认、未修改状态展示每个库的全套交互式原生组件,不做任何主题覆盖和自定义封装。
审计工具链分三轮运行。第一轮为自动化axe-core 4.11扫描,通过Playwright对渲染后的DOM进行扫描,启用完整WCAG 2.2 AA规则集及实验性规则。第二轮为人工键盘测试:对每个原生组件按照WAI-ARIA创作实践指南预期的键盘契约,测试Tab、Shift+Tab、方向键、Escape、Enter、Space及Home/End。第三轮为屏幕阅读器冒烟测试:在Chrome上使用NVDA 2025.1,在macOS Sonoma上使用Safari/VoiceOver,验证角色播报、无障碍名称以及用户交互时的状态播报。
我们选择11个ARIA模式作为矩阵的测试面,因为这些模式出现在曾遭到诉讼的产品UI中:对话框、警告对话框、组合框、列表框、菜单、菜单栏、标签页、手风琴、工具提示、开关和滑块。一个库在按钮和标题上表现完美,却提交了一个破损的组合框,那么当真实用户第一次尝试筛选客户列表时,该库就会在审计中失败。
axe通过意味着在使用默认属性和库的官方示例用法渲染时,WCAG 2.2 AA标签及实验性标签下的规则零违规。这并不代表该库无懈可击——axe大约能捕捉所有WCAG失败的一半——但一个在自己演示页面上无法通过axe的库,在其他任何地方也不会通过。
“默认状态是大多数工程团队唯一会看到的状态。如果一个库提交了一个有问题的默认值,这个有问题的默认值就会上线到生产环境。”
2. 七款库并排对比
七款库中,Radix UI、React Aria Components和Ark UI在默认状态下所有原生组件均通过axe,无需任何覆盖。Headless UI除菜单原生组件外全部通过——在列表框式菜单触发器上缺少aria-activedescendant,触发了一处违规。shadcn/ui本质上是Radix UI的薄封装,其所提供的每个原生组件均通过,但问题在于它仅覆盖了Radix约三分之二的组件面,且缺失的部分(组合框、列表框、菜单栏)恰恰是供应商在自行实现时最常出现无障碍问题的模式。
Mantine和Chakra UI v3是两款默认状态下存在axe违规的库。Mantine的组合框、开关和滑块在官方示例用法中均产生了至少一处axe违规,大多与底层输入元素缺少无障碍名称有关。Chakra UI v3于2024年底迁移至基于Zag的状态机架构,修复了许多v2问题,但其工具提示仍仅在悬停时触发,这在axe中构成1.4.13”悬停或聚焦时的内容”违规,并在屏幕阅读器虚拟模式下存在键盘捕获风险。
3. 模式覆盖矩阵
下方的11模式网格是核心参考。绿色单元格表示该库原生提供该原生组件,且默认状态下通过axe。黄色单元格表示该库提供该原生组件,但在官方示例用法中出现了至少一处axe违规、键盘契约缺口或屏幕阅读器缺口。灰色”N/A”表示该库不提供该原生组件——对于shadcn/ui,有三个模式需要直接导入Radix或引入第三方库。
| 模式 | Radix | React Aria | Ark UI | shadcn | Headless | Mantine | Chakra v3 |
|---|---|---|---|---|---|---|---|
| 对话框 | 通过 | 通过 | 通过 | 通过 | 通过 | 通过 | 通过 |
| 警告对话框 | 通过 | 通过 | 通过 | 通过 | 通过 | 部分 | 通过 |
| 组合框 | 通过 | 通过 | 通过 | N/A | 通过 | 部分 | 通过 |
| 列表框 | 通过 | 通过 | 通过 | N/A | 通过 | 通过 | 通过 |
| 菜单 | 通过 | 通过 | 通过 | 通过 | 部分 | 通过 | 通过 |
| 菜单栏 | 通过 | 通过 | 通过 | N/A | N/A | 通过 | 通过 |
| 标签页 | 通过 | 通过 | 通过 | 通过 | 通过 | 通过 | 通过 |
| 手风琴 | 通过 | 通过 | 通过 | 通过 | 通过 | 通过 | 通过 |
| 工具提示 | 通过 | 通过 | 通过 | 通过 | 通过 | 通过 | 部分 |
| 开关 | 通过 | 通过 | 通过 | 通过 | 通过 | 部分 | 通过 |
| 滑块 | 通过 | 通过 | 通过 | 通过 | 通过 | 部分 | 通过 |
灰色N/A单元格不是失败——它是一个采购问题。shadcn/ui的逻辑是:从精选集合中复制所需源代码,然后发布;对于三个N/A模式,您可以直接导入Radix原生组件,或从姐妹注册表中引入。shadcn/ui的风险不在于它所提供的组件——这些组件继承了Radix的合规性——而在于它不提供的组件,那是团队在午夜赶deadline时会手写一个组合框的地方。
4. 失效的键盘契约
各库中最为普遍的失效并非缺少aria属性——而是键盘契约与APG几乎吻合,却在某个按键上出现偏差。Mantine的组合框不响应APG规定的Home和End键。Chakra v3的工具提示在通过悬停打开后无法通过Escape关闭。Headless UI的菜单原生组件在第一次ArrowDown时收起,而不是将焦点放在第一个选项上,因为其实现将触发元素视为打开时的活动后代。
这些都不是生僻的边界情况,而是屏幕阅读器用户第一分钟就会触发的操作模式。下方的对比将同一个组合框API用两种方式实现——一次使用Mantine的默认值,一次使用React Aria Components——以展示将键盘契约视为规范而非锦上添花时的实际效果。
<Combobox
store={combobox}
onOptionSubmit={(v) => setValue(v)}
>
<Combobox.Target>
<InputBase
// 输入框上没有aria-controls连线
// Home/End键不在列表框内移动焦点
// ArrowDown打开但不将焦点放在第1项
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
/>
</Combobox.Target>
<Combobox.Dropdown>
{/* 列表框选项在此渲染 */}
</Combobox.Dropdown>
</Combobox><ComboBox aria-label="Filter customers">
<Label>Customer</Label>
<Input />
<Popover>
<ListBox>
<ListBoxItem>Alpha</ListBoxItem>
<ListBoxItem>Bravo</ListBoxItem>
<ListBoxItem>Charlie</ListBoxItem>
</ListBox>
</Popover>
</ComboBox>
// aria-controls、aria-activedescendant、
// aria-expanded、role=combobox、Home/End、
// PageUp/PageDown、Escape:均默认已连线。从已发布规范派生键盘契约的库——React Aria来自WAI-ARIA APG,Ark UI来自Zag.js状态机——将这些契约作为组件支持的唯一行为进行提交。将键盘契约视为功能列表的库,大约实现了其中80%,其余20%留作”未来工作”。而这20%恰恰是辅助技术用户最常按的按键。
5. 无障碍的包体积成本
选择严格库时最常见的反对理由是包体积。审计结果并未支持这一观点。Radix UI原生组件支持按组件进行树摇,每个原生组件的gzip压缩后大小约为7至15 KB;React Aria Components更重——完整包gzip后约95 KB——但同样支持树摇。Headless UI是其中最轻的,整个包gzip后约25 KB。Mantine和Chakra v3均属于功能齐全型,在同一包中附带样式系统,开箱即用的gzip大小约为180至220 KB。
这种权衡是真实存在的,但比讨论所呈现的要小。一个不响应Home和End的组合框与一个正确响应的组合框,字节成本大致相同。选择并非”无障碍vs性能”——而是”规范派生行为vs手写行为”,而手写版本往往更大,因为它携带了自己的临时状态机。
6. 选型实操手册
如果产品是企业级应用且有采购驱动的无障碍要求,选React Aria Components。
它是所有被审计库中唯一API明确派生自WAI-ARIA创作实践指南的库。包比Radix重,但向VPAT审核方说明审计通过情况最为简洁,因为每个原生组件都有已发布的合规声明。
如果团队自主维护设计系统,选Radix UI(可选在其上叠加shadcn/ui)。
Radix提供无样式、规范合规的原生组件。shadcn/ui让它们易于复制引入并主题化。需要注意的是shadcn未提供的三个模式——组合框、列表框、菜单栏——应直接导入Radix,而非手写。
如果团队以Tailwind为主且只需要有限组件面,选Headless UI——但请审计自己代码中的菜单原生组件。
Headless UI是该领域包体积最小的,提供一个小而经过良好测试的组件面。菜单原生组件的一处缺口已在上文说明;通过在列表框式菜单上显式连线aria-activedescendant,或用项目本地helper进行封装来修复。
如果团队重视开箱即用胜过规范合规,选Mantine或Chakra v3——但请规划好覆盖工作。
两款库上手快且开箱即用效果好。但两者都需要针对各组件做额外覆盖,才能通过组合框、开关、滑块(Mantine)或工具提示(Chakra v3)的审计。请将覆盖工作排入引入该库的迭代,而非在审计失败的迭代中亡羊补牢。
在引入库之前,对其官方演示运行axe。
各供应商均维护演示站点。将axe DevTools对准它们。如果供应商自己的演示在您计划使用的原生组件上出现违规,这些违规将随之进入您的产品。这一五分钟的检查将您要引入的库与要回避的库区分开来。
结论:规范即契约
能够干净通过axe审计的库,是那些作者将WAI-ARIA创作实践指南视为契约而非参考的库。Radix UI、React Aria Components和Ark UI分别从已发布规范派生其键盘契约和ARIA连线——Radix和React Aria来自APG,Ark UI来自Zag.js状态机——并按规范提交,而非规范的子集。
那些失败的库,并非因为其作者不关心无障碍。它们失败,是因为键盘契约被视为功能列表,而功能列表最终会在80%时停下,而非100%。最后那20%——组合框中的Home和End、用Escape关闭悬停打开的工具提示、菜单中第一次ArrowDown时的焦点管理——是用户注意到的部分。
shadcn/ui的情况横跨两个类别。它所提供的组件继承了Radix的规范派生性并通过测试。它不提供的组件是团队手写内部组合框的地方,而那个内部组合框正是下一个axe违规进入代码库的入口。解决方案不是换一个库——而是对那三个模式直接导入Radix,并以和设计系统其他部分同等的认真程度对待它们。
引入库的工程团队应将选型与开发者专区中的其他开发者工具配套使用,在将库发布到生产环境之前对其演示页面运行免费WCAG 2.2扫描,并将结果与完整WCAG 2.2成功标准参考进行基准对比。
“选一个作者将规范视为契约的库,审计就只是走个形式。选一个作者将规范视为参考的库,审计就变成了积压工单。”