ui-ux-pro-max-skill 中的 shadcn/ui 无障碍模式实战:从 Radix 原语到 WCAG 测试闭环
【免费下载链接】ui-ux-pro-max-skillAn AI skill that provides design intelligence for building professional UI/UX across multiple platforms.项目地址: https://gitcode.com/gh_mirrors/ui/ui-ux-pro-max-skill
本文基于 ui-ux-pro-max-skill 仓库中ui-styling技能的可访问性参考文档,系统讲解 shadcn/ui 组件体系的无障碍实现模式:Radix UI 原语如何提供键盘导航与屏幕阅读器支持、表单如何正确关联标签与错误信息、以及颜色对比度、焦点指示、减弱动效等 WCAG 要求的落地写法。读完后你可以直接按文档中的代码模式编写 Dialog、表单、Tabs 等可访问组件,并套用仓库内的测试清单与 axe-core 自动化方案完成验收。
该文档位于 shadcn-accessibility.md,是 ui-styling 技能入口 SKILL.md 的三篇核心参考之一(另两篇是组件目录与主题定制),并被 Accessibility Patterns 章节明确引用,用于指导 AI Agent 在生成 React + shadcn/ui 界面时遵循 ARIA 模式、键盘导航与屏幕阅读器支持。仓库中还存在一份与之完全一致的同步副本 cli/assets/skills/ui-styling/references/shadcn-accessibility.md,供 CLI 发布资产使用。
基础:Radix UI 原语
文档开篇指出一个核心前提:shadcn/ui 构建在 Radix UI 原语之上,这些原语是无样式的、遵循 WAI-ARIA 设计模式的可访问组件。这一设计决策为开发者带来了五项开箱即得的能力:
- 内建的键盘导航;
- 屏幕阅读器播报;
- 焦点管理;
- 自动应用的 ARIA 属性;
- 针对无障碍标准的测试验证。
这意味着在 shadcn/ui 生态中编写无障碍界面时,大部分 ARIA 属性(如aria-expanded、aria-controls、role等)由原语层自动处理,开发者只需关注语义结构与可见焦点等"外层"问题。ui-styling 技能的 SKILL.md 也把这一点列为组件层的核心特性:"Pre-built accessible components via Radix UI primitives"。
前置条件是完成 shadcn/ui 初始化。参考 shadcn-components.md 的安装说明:
npx shadcn@latest add button npx shadcn@latest add button card dialog # 批量添加 npx shadcn@latest add --all # 添加全部组件组件会安装到components/ui/目录并自动处理依赖。仓库内还附带了一个 Python 封装脚本 shadcn_add.py,它先检查项目根目录是否存在components.json(即 shadcn 是否初始化),再读取package.json中锁定的 shadcn 版本(找不到时回退到内置的2.3.0默认值),最后拼装并执行npx shadcn@<version> add <components>命令。其 CLI 用法与参数如下:
# 添加单个组件 python shadcn_add.py button # 批量添加 python shadcn_add.py button card dialog # 添加全部组件 python shadcn_add.py --all # 覆盖已存在组件 python shadcn_add.py button --overwrite # 仅预览将执行的命令 python shadcn_add.py button card --dry-run # 列出已安装组件 python shadcn_add.py --list从 shadcn_add.py 的参数定义看,--list、--all、--overwrite、--dry-run、--project-root均为可选参数;脚本对"未初始化""组件已存在""npx 缺失"等失败路径都返回明确错误信息与退出码,适合作为 Agent 的幂等安装手段。
键盘导航
焦点管理
可见焦点状态。键盘用户依赖focus-visible变体区分"鼠标点击"与"键盘聚焦"。文档给出的标准写法:
<Button className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"> Accessible Button </Button>跳转链接(Skip to content)。这是键盘用户快速越过重复导航的关键模式:
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2"> Skip to content </a> <main id="main-content"> {/* Content */} </main>链接平时被sr-only隐藏,键盘 Tab 聚焦时才通过focus:not-sr-only显示出来。仓库数据层的 ux-guidelines.csv 中 Skip Links 条目(第 45 行)同样将其列为 Medium 严重性要求:"Allow keyboard users to skip navigation",反面案例是"100 次 Tab 才能到达内容"。
Dialog/Modal 导航
Dialog 通过 Radix Dialog 原语自动实现焦点陷阱:
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog" <Dialog> <DialogTrigger>Open</DialogTrigger> <DialogContent> {/* Focus trapped here */} <input /> {/* Auto-focused */} <Button>Action</Button> {/* Esc to close, Tab to navigate */} </DialogContent> </Dialog>其行为特性包括:焦点被锁定在对话框内、Esc键关闭、Tab在可聚焦元素间循环、关闭后焦点自动返回触发元素。
需要注意的是,文档中这个最小示例只展示了焦点行为;而在 ux-guidelines.csv 的 Dialog 相关条目(第 13 行)中,"Include proper dialog structure" 被列为 High 严重性:完整的 Dialog 必须包含DialogHeader、DialogTitle、DialogDescription,缺 title 或 description 会被判为缺陷——屏幕阅读器正是依靠这两个元素来播报弹窗上下文。shadcn-components.md 的 Dialog 章节 给出的完整结构如下:
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" <Dialog> <DialogTrigger asChild> <Button>Open</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Are you sure?</DialogTitle> <DialogDescription>This action cannot be undone.</DialogDescription> </DialogHeader> </DialogContent> </Dialog>Dropdown/Menu 导航
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" <DropdownMenu> <DropdownMenuTrigger>Open</DropdownMenuTrigger> <DropdownMenuContent> <DropdownMenuItem>Profile</DropdownMenuItem> <DropdownMenuItem>Settings</DropdownMenuItem> <DropdownMenuItem>Logout</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu>键盘快捷键约定:
| 按键 | 行为 |
|---|---|
Space/Enter | 打开菜单 |
Arrow Up/Down | 在菜单项间移动 |
Esc | 关闭菜单 |
Tab | 关闭并将焦点移出 |
Command Palette 导航
Command 组件(命令面板)同样遵循完整的键盘模型:
import { Command } from "@/components/ui/command" <Command> <CommandInput placeholder="Search..." /> <CommandList> <CommandGroup heading="Suggestions"> <CommandItem>Calendar</CommandItem> <CommandItem>Search</CommandItem> </CommandGroup> </CommandList> </Command>特性:输入即过滤(type to filter)、方向键导航、Enter选择、Esc关闭。仓库的 ux-guidelines.csv 第 22-23 行还补充了两条相关约定:使用 Command 承载搜索与可搜索列表(而非Input加自定义下拉),以及用CommandGroup的heading为条目分组。
屏幕阅读器支持
语义化 HTML
优先使用原生 HTML 元素,而不是"div 汤":
// Good: Semantic HTML <button>Click me</button> <nav><a href="/">Home</a></nav> // Avoid: Div soup <div onClick={handler}>Click me</div>ux-guidelines.csv 中 Screen Reader 条目(第 43 行)与之一致:正确内容在被朗读时应语义完整,正面案例是<nav>、<main>、<article>等结构化标签。
ARIA 标签
为交互元素命名。图标按钮必须通过aria-label提供可访问名称:
<Button aria-label="Close dialog"> <X className="h-4 w-4" /> </Button> <Input aria-label="Email address" type="email" />描述元素。当可见文本不足以传达后果时,用aria-describedby关联补充说明:
<Button aria-describedby="delete-description"> Delete Account </Button> <p id="delete-description" className="sr-only"> This action permanently deletes your account and cannot be undone </p>ux-guidelines.csv第 41 行将 ARIA Labels 定为 High 严重性:"Add aria-label for icon-only buttons",并明确反面模式是无标签的<button><Icon/></button>。
仅屏幕阅读器可见的文本
对纯图标按钮,用sr-only类内嵌辅助文本,比aria-label更贴近 DOM 语义:
<Button> <Trash className="h-4 w-4" /> <span className="sr-only">Delete item</span> </Button> // CSS for sr-only .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; }其原理是把元素压缩到 1px 并裁剪切出视口,使其对视觉用户不可见但保留在可访问性树中。
实时区域(Live Regions)
动态内容变更需要主动播报。aria-live="polite"会在用户空闲时插入播报,aria-atomic="true"保证整块区域被完整读出;紧急更新(如错误)使用assertive:
<div aria-live="polite" aria-atomic="true"> {message} </div> // For urgent updates <div aria-live="assertive"> {error} </div>shadcn/ui 的 Toast 组件内部已包含 live region,因此toast()发出的通知会自动播报:
const { toast } = useToast() toast({ title: "Success", description: "Profile updated" }) // Announced to screen readers automatically仓库数据层对错误播报的要求(ux-guidelines.csv 第 44 行,High 严重性)是"Use aria-live or role=alert for errors",仅靠红色边框这类视觉指示是不及格的。
表单无障碍
标签与描述
输入框必须有标签。Label的htmlFor与Input的id必须配对:
import { Label } from "@/components/ui/label" import { Input } from "@/components/ui/input" <div> <Label htmlFor="email">Email</Label> <Input id="email" type="email" /> </div>在 Form 场景中加入描述信息:
import { FormDescription, FormMessage } from "@/components/ui/form" <FormItem> <FormLabel>Username</FormLabel> <FormControl> <Input {...field} /> </FormControl> <FormDescription> Your public display name </FormDescription> <FormMessage /> {/* Error messages */} </FormItem>错误处理
校验失败时,错误不仅要可见,还要被屏幕阅读器关联到出错的字段。核心是三个 ARIA 属性的组合:aria-invalid标记状态、aria-describedby指向错误信息节点、FormMessage携带对应id:
<FormField control={form.control} name="email" render={({ field, fieldState }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl> <Input {...field} aria-invalid={!!fieldState.error} aria-describedby={fieldState.error ? "email-error" : undefined} /> </FormControl> <FormMessage id="email-error" /> </FormItem> )} />SKILL.md 的 Common Patterns 章节 还给出了配套的完整表单骨架:react-hook-form+zodResolver做 schema 校验(z.object({ email: z.string().email(), password: z.string().min(8) })),再包进<Form {...form}>与FormField。ux-guidelines.csv第 110 行(第 109 号条目,High 严重性)进一步给出进阶要求:表单整体校验失败时,应在顶部提供错误摘要(error summary),提交失败后将焦点移动到摘要标题,并为每条错误提供指向对应字段的链接——这比仅显示 toast 更利于键盘与屏幕阅读器用户。
必填字段
用可见星号加sr-only文本双通道传达"必填":
<Label htmlFor="name"> Name <span className="text-destructive">*</span> <span className="sr-only">(required)</span> </Label> <Input id="name" required />视觉用户看到红色星号,屏幕阅读器读到 "(required)"。
Fieldset 与 Legend
用fieldset/legend对一组相关字段进行语义分组:
<fieldset> <legend className="text-lg font-semibold mb-4"> Contact Information </legend> <div className="space-y-4"> <FormField name="email" /> <FormField name="phone" /> </div> </fieldset>这是 WAI-ARIA 之外的原生 HTML 分组方案,屏幕阅读器会在进入该组时先朗读legend内容。
组件专项模式
以下五个组件的模式覆盖了"自动 ARIA 属性由 Radix 原语提供"这一主题的典型验证:
Accordion
import { Accordion } from "@/components/ui/accordion" <Accordion type="single" collapsible> <AccordionItem value="item-1"> <AccordionTrigger> {/* Includes aria-expanded, aria-controls automatically */} Is it accessible? </AccordionTrigger> <AccordionContent> {/* Hidden when collapsed, announced when expanded */} Yes. Follows WAI-ARIA design pattern. </AccordionContent> </AccordionItem> </Accordion>AccordionTrigger自动携带aria-expanded与aria-controls;AccordionContent在折叠时对辅助技术隐藏、展开后参与播报。
Tabs
import { Tabs } from "@/components/ui/tabs" <Tabs defaultValue="account"> <TabsList role="tablist"> {/* Arrow keys navigate, Space/Enter activates */} <TabsTrigger value="account">Account</TabsTrigger> <TabsTrigger value="password">Password</TabsTrigger> </TabsList> <TabsContent value="account"> {/* Hidden unless selected, aria-labelledby links to trigger */} Account content </TabsContent> </Tabs>键盘模型为:方向键在标签间移动,Space/Enter激活;TabsContent未选中时不进入可访问性树,且通过aria-labelledby关联到对应 trigger。
Select
import { Select } from "@/components/ui/select" <Select> <SelectTrigger aria-label="Choose theme"> <SelectValue placeholder="Theme" /> </SelectTrigger> <SelectContent> {/* Keyboard navigable, announced to screen readers */} <SelectItem value="light">Light</SelectItem> <SelectItem value="dark">Dark</SelectItem> </SelectContent> </Select>下拉内容可键盘导航并向屏幕阅读器播报。ux-guidelines.csv 第 21 行强调 Select 的完整结构:SelectTrigger+SelectValue+SelectContent+SelectItem,缺SelectValue或SelectContent属于 High 严重性缺陷。
Checkbox 与 Radio
import { Checkbox } from "@/components/ui/checkbox" import { Label } from "@/components/ui/label" <div className="flex items-center space-x-2"> <Checkbox id="terms" aria-describedby="terms-description" /> <Label htmlFor="terms">Accept terms</Label> </div> <p id="terms-description" className="text-sm text-muted-foreground"> You agree to our Terms of Service and Privacy Policy </p>注意这里把"法律含义"放在可见的aria-describedby关联段落中——这比把条款塞进placeholder更稳妥。Radio Group 的标准结构(RadioGroupItem配id、Label配htmlFor、defaultValue在RadioGroup上)见 shadcn-components.md。
Alert
import { Alert } from "@/components/ui/alert" <Alert role="alert"> {/* Announced immediately to screen readers */} <AlertTitle>Error</AlertTitle> <AlertDescription> Your session has expired </AlertDescription> </Alert>role="alert"等价于aria-live="assertive"+aria-atomic="true",内容一旦挂载即被立即播报,适合会话过期这类紧急通知。
颜色对比度
文档给出的 WCAG 门槛:
- AA:正文 4.5:1,大文本 3:1;
- AAA:正文 7:1,大文本 4.5:1。
默认写法检查:
// Good: High contrast <p className="text-gray-900 dark:text-gray-100">Text</p> // Avoid: Low contrast <p className="text-gray-400 dark:text-gray-600">Hard to read</p>次要文本应使用语义化的 muted foreground 变量而不是任意灰色:
// Use semantic muted foreground <p className="text-muted-foreground"> Secondary text with accessible contrast </p>这与 shadcn-theming.md 的 CSS 变量体系呼应:muted/muted-foreground是一组语义 token,在:root与.dark下分别取值。仓库的 ux-guidelines.csv 中 Color Contrast 条目(High 严重性)给出了量化参照:#333on white 约 7:1 合格,#999on white 仅 2.8:1 不合格。同一文件还有 Color Only 条目:不要用颜色单独传达信息,错误状态应为"红色文本 + 错误图标"而非仅红边框。
焦点指示器
原则是"永远提供可见焦点指示器",并优先使用focus-visible:变体:
默认焦点环:
<Button className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"> Button </Button>自定义焦点样式:
<a href="#" className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:underline"> Link </a>不要移除焦点样式:
// Avoid <button className="focus:outline-none">Bad</button> // Use focus-visible instead <button className="focus-visible:ring-2">Good</button>仓库的 ux-guidelines.csv 为此补充了两条 WCAG 2.2 级别的量化要求:Focus Not Obscured(AA,High 严重性)——键盘焦点必须至少部分可见,sticky 头部需要用scroll-padding偏移,避免fixed浮层完全覆盖焦点;Focus Appearance(AAA,Medium)——焦点指示器至少 2 CSS px 周长、与周围有 3:1 状态对比度,参考写法是outline: 2px solid currentColor; outline-offset: 2px,而低对比度的 1pxbox-shadow是反面案例。
动效与减弱动效偏好
尊重系统级prefers-reduced-motion设置。全局兜底:
@media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } }组件级则用 Tailwind 的motion-reduce:变体:
<div className="transition-all motion-reduce:transition-none"> Respects user preference </div>ux-guidelines.csv 中 Motion Sensitivity 条目(High 严重性)进一步指出:视差滚动、scroll-jacking 会造成眩晕感,正确做法是尊重prefers-reduced-motion并直接呈现最终可读状态,而不是在减弱模式下保留简化版视差。
测试清单与自动化工具
文档给出的验收清单(14 项):
- 所有交互元素可键盘访问
- 焦点指示器可见
- 屏幕阅读器能正确播报所有内容
- 表单错误被播报且已关联到字段
- 颜色对比度满足 WCAG AA
- 使用语义化 HTML
- 图标按钮提供 ARIA 标签
- 模态/对话框焦点陷阱生效
- 下拉/Select 可键盘导航
- Live region 播报动态更新
- 尊重减弱动效偏好
- 浏览器缩放至 200% 仍可用
- Tab 顺序符合逻辑
- 提供跳转导航的 Skip 链接
配套工具链分两类:
手动/工具测试:Lighthouse 无障碍审计、axe DevTools 浏览器扩展、NVDA/JAWS 屏幕阅读器、纯键盘导航测试、对比度检测工具(如 WebAIM Contrast Checker)。
开发期自动化:安装@axe-core/react后,仅在开发环境按固定间隔扫描渲染树:
npm install -D @axe-core/reactimport { useEffect } from 'react' if (process.env.NODE_ENV === 'development') { import('@axe-core/react').then((axe) => { axe.default(React, ReactDOM, 1000) }) }第三个参数1000是扫描间隔(毫秒)。该模式把 a11y 缺陷的反馈周期从"上线前审计"压缩到"开发时实时告警",与前述的 Radix 原语 + ARIA 模式构成"生成即合规"的闭环:原语保证交互层语义,本文模式保证应用层语义,axe-core 在开发期兜底。
与仓库其他资源的衔接
- 本文所有组件 API(Dialog、Accordion、Tabs、Select、Form 等)的完整用法与变体取值,见 shadcn-components.md;
muted-foreground、ring等语义颜色变量的定义与深色模式实现,见 shadcn-theming.md;- 仓库数据层的 ux-guidelines.csv 以"条目 + Do/Don't + 严重性"格式维护了 100 余条跨技术栈的 UX/无障碍准则,其中 Accessibility 分类条目(对比度、aria-label、键盘导航、表单标签、错误播报、Skip 链接、目标尺寸 24px、焦点不可被遮挡等)可作为本文清单之外的通用扩展参考;
- 组件安装环节可通过 shadcn_add.py 的
--dry-run/--list参数在改动前预演与核对,确保"先npx shadcn@latest init再 add"的初始化顺序不被跳过(脚本在未检测到components.json时会直接报错退出)。
综上,这份参考文档的价值在于把"shadcn/ui 是可访问的"这一笼统结论拆成了可执行的五层模式:Radix 原语(交互层)、键盘与焦点管理(导航层)、ARIA 与 live region(播报层)、表单语义(数据层)、对比度/动效/测试(验收层),并全部给出了可直接复制的 TSX 代码与 Tailwind 类名。
【免费下载链接】ui-ux-pro-max-skillAn AI skill that provides design intelligence for building professional UI/UX across multiple platforms.项目地址: https://gitcode.com/gh_mirrors/ui/ui-ux-pro-max-skill
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考