accessibility-checklist
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAccessibility Review Checklist
可访问性审查清单
How to Use This Checklist
如何使用本清单
- Review changed components against the relevant sections below
- Not every section applies to every component — form checks only apply to form components, modal checks only apply to modals, etc.
- This codebase uses Radix UI / shadcn/ui extensively. These libraries handle most a11y patterns (keyboard nav, focus management, ARIA) automatically. Your primary job is to catch misuse of the library, not absence of manual implementation.
- When unsure whether a component library handles a pattern, lower confidence rather than asserting
- 根据下方相关章节审查变更后的组件
- 并非每个章节都适用于所有组件——表单检查仅适用于表单组件,模态框检查仅适用于模态框等
- 本代码库广泛使用Radix UI / shadcn/ui。这些库会自动处理大多数可访问性(a11y)模式(键盘导航、焦点管理、ARIA)。你的主要工作是发现库的误用,而非检查是否缺少手动实现的内容。
- 若不确定组件库是否处理了某种模式,应降低判断的确定性,而非贸然断言
§1 Component Library Misuse (Radix / shadcn/ui)
§1 组件库误用(Radix / shadcn/ui)
This is the highest-signal section for this codebase. Radix handles a11y correctly when used correctly — bugs come from misuse.
-
Dialog/Sheet without title: Radixand
DialogrequireSheet/DialogTitlefor screen reader announcement. IfSheetTitleis omitted or visually hidden withoutDialogTitleonaria-label, screen readers announce an unlabeled dialog.DialogContent- Common violation: with no
<DialogContent>and no<DialogTitle>aria-label - Note: Using is a valid pattern for dialogs where a visible title doesn't fit the design
<VisuallyHidden><DialogTitle>...</DialogTitle></VisuallyHidden>
- Common violation:
-
AlertDialog without description:should include
AlertDialogContentfor screen readers to understand the confirmation context. If omitted, addAlertDialogDescriptionto explicitly opt out (otherwise Radix warns).aria-describedby={undefined} -
Select/Combobox without accessible trigger label: Radixneeds
Selecton the trigger when there's no visible label. Customaria-labelandgeneric-select.tsxwrappers should propagate labels.generic-combo-box.tsx- Common violation: inside a form field that has a visual label, but the label isn't associated via
<Select>or wrappinghtmlFor
- Common violation:
-
DropdownMenu items without accessible names: Icon-only menu items need text content or. Menu items that are just icons (e.g., copy, delete, edit) need text.
aria-label- Correct pattern: (icon + text)
<DropdownMenuItem><TrashIcon /> Delete</DropdownMenuItem> - Violation: (icon only, no text, no aria-label)
<DropdownMenuItem><TrashIcon /></DropdownMenuItem>
- Correct pattern:
-
Tooltip as only accessible name: Tooltip text is not reliably announced by all screen readers. If a control's only accessible name is in a tooltip, it needsas well.
aria-label- Common pattern to flag: — Button needs
<Tooltip><TooltipTrigger><Button><Icon /></Button></TooltipTrigger><TooltipContent>Delete</TooltipContent></Tooltip>aria-label="Delete"
- Common pattern to flag:
-
Overriding Radix's keyboard handling: If a component wraps a Radix primitive and addsthat calls
onKeyDownore.preventDefault(), it may break Radix's built-in keyboard navigation.e.stopPropagation()
这是针对本代码库信号最强的章节。正确使用Radix时,它能正确处理可访问性问题——错误源于误用。
-
无标题的Dialog/Sheet:Radix的和
Dialog需要Sheet/DialogTitle供屏幕阅读器播报。如果省略SheetTitle,或在DialogTitle上未设置DialogContent就将其视觉隐藏,屏幕阅读器会播报一个未标记的对话框。aria-label- 常见违规:既没有
<DialogContent>也没有<DialogTitle>aria-label - 注意:对于不需要可见标题的对话框,使用是有效的模式
<VisuallyHidden><DialogTitle>...</DialogTitle></VisuallyHidden>
- 常见违规:
-
无描述的AlertDialog:应包含
AlertDialogContent,以便屏幕阅读器理解确认上下文。如果省略,需添加AlertDialogDescription以明确选择退出(否则Radix会发出警告)。aria-describedby={undefined} -
无可访问触发标签的Select/Combobox:当没有可见标签时,Radix的需要在触发器上设置
Select。自定义的aria-label和generic-select.tsx包装器应传递标签。generic-combo-box.tsx- 常见违规:位于有视觉标签的表单字段内,但标签未通过
<Select>或包裹方式关联htmlFor
- 常见违规:
-
无可访问名称的DropdownMenu项:仅含图标菜单项需要文本内容或。仅由图标组成的菜单项(如复制、删除、编辑)需要添加文本。
aria-label- 正确模式:(图标+文本)
<DropdownMenuItem><TrashIcon /> Delete</DropdownMenuItem> - 违规:(仅图标,无文本,无aria-label)
<DropdownMenuItem><TrashIcon /></DropdownMenuItem>
- 正确模式:
-
仅用Tooltip作为可访问名称:Tooltip文本并非所有屏幕阅读器都能可靠播报。如果控件的唯一可访问名称在Tooltip中,还需要同时设置。
aria-label- 需标记的常见模式:——Button需要添加
<Tooltip><TooltipTrigger><Button><Icon /></Button></TooltipTrigger><TooltipContent>Delete</TooltipContent></Tooltip>aria-label="Delete"
- 需标记的常见模式:
-
覆盖Radix的键盘处理逻辑:如果组件包装了Radix原语并添加了调用或
e.preventDefault()的e.stopPropagation(),可能会破坏Radix内置的键盘导航功能。onKeyDown
§2 Forms & Labels
§2 表单与标签
The codebase uses react-hook-form + Zod with shadcn/ui's component, which auto-associates labels via context. Issues arise when forms bypass this pattern.
FormFormItem-
Every form input must have an accessible name: Via,
<FormLabel>,<label htmlFor={id}>, oraria-label. Placeholder text alone is NOT a label.aria-labelledby- Common violation: Custom inputs outside /
<FormField>that don't get auto-association<FormItem> - Common violation: used standalone without any label
<Input placeholder="Enter name" />
- Common violation: Custom inputs outside
-
Error messages must be associated with their input: shadcn/ui'sauto-associates via
<FormMessage>when insidearia-describedby. Custom error rendering outside this pattern loses the association.<FormItem>- Flag: Error text rendered near an input but not using or manual
<FormMessage>aria-describedby
- Flag: Error text rendered near an input but not using
-
Required fields must be indicated programmatically: Useor native
aria-required="true", not just a visual asterisk. Therequiredcomponent doesn't add this automatically — it comes from the Zod schema validation at submit time, not at the HTML level.Form -
Grouped controls need group semantics: Radio groups and checkbox groups should use(Radix) or
<RadioGroup>/<fieldset>. Loose radio buttons or checkboxes without group context confuse screen readers.<legend>- Scope: Configuration pages, settings forms, multi-option selectors
本代码库使用react-hook-form + Zod搭配shadcn/ui的组件,该组件会通过上下文自动关联标签。当表单绕过此模式时会出现问题。
FormFormItem-
每个表单输入必须有可访问名称:通过、
<FormLabel>、<label htmlFor={id}>或aria-label实现。仅靠占位符文本不能作为标签。aria-labelledby- 常见违规:在/
<FormField>之外的自定义输入未获得自动关联<FormItem> - 常见违规:单独使用而未添加任何标签
<Input placeholder="Enter name" />
- 常见违规:在
-
错误消息必须与对应的输入关联:shadcn/ui的在
<FormMessage>内时会通过<FormItem>自动关联。在此模式外自定义错误渲染会丢失关联。aria-describedby- 标记:在输入附近渲染但未使用或手动设置
<FormMessage>的错误文本aria-describedby
- 标记:在输入附近渲染但未使用
-
必填字段必须通过程序方式标记:使用或原生
aria-required="true"属性,不能仅靠视觉星号。required组件不会自动添加此属性——它来自提交时的Zod模式验证,而非HTML层面。Form -
分组控件需要分组语义:单选按钮组和复选框组应使用(Radix)或
<RadioGroup>/<fieldset>。无分组上下文的零散单选按钮或复选框会让屏幕阅读器产生混淆。<legend>- 适用范围:配置页面、设置表单、多选项选择器
§3 Accessible Names (Icons & Buttons)
§3 可访问名称(图标与按钮)
With 48 shadcn/ui components and heavy icon usage (Lucide React), icon-only interactive elements are a primary risk area.
-
Icon-only buttons must have: Buttons containing only an icon (no visible text) need
aria-labeldescribing the action.aria-label- Common violation: without
<Button variant="ghost" size="icon"><TrashIcon /></Button>aria-label - Very common in: data tables (row actions), toolbars, card headers, dialog close buttons
- Note: shadcn/ui's close button already includes
Dialog— don't flag this<span className="sr-only">Close</span>
- Common violation:
-
Icon-only links need accessible names: Same as buttons —or
<a>with only an icon needs<Link>.aria-label -
text is a valid alternative to
sr-only:aria-labelis correct. Don't flag this pattern as missing a label.<Button><TrashIcon /><span className="sr-only">Delete item</span></Button> -
Decorative icons should be hidden: Icons that are purely decorative (next to visible text) should haveto avoid redundant announcements.
aria-hidden="true"- Correct:
<Button><PlusIcon aria-hidden="true" /> Add item</Button> - Also correct: Lucide icons may set by default — check before flagging
aria-hidden
- Correct:
由于使用了48个shadcn/ui组件且大量使用图标(Lucide React),仅含图标的交互元素是主要风险点。
-
仅含图标的按钮必须设置:仅包含图标(无可见文本)的按钮需要
aria-label来描述操作。aria-label- 常见违规:未设置
<Button variant="ghost" size="icon"><TrashIcon /></Button>aria-label - 常见场景:数据表(行操作)、工具栏、卡片头部、对话框关闭按钮
- 注意:shadcn/ui的关闭按钮已包含
Dialog——无需标记此情况<span className="sr-only">Close</span>
- 常见违规:
-
仅含图标的链接需要可访问名称:与按钮相同——仅含图标的或
<a>需要<Link>。aria-label -
文本是
sr-only的有效替代方案:aria-label是正确的。不要将此模式标记为缺少标签。<Button><TrashIcon /><span className="sr-only">Delete item</span></Button> -
装饰性图标应隐藏:纯装饰性图标(位于可见文本旁)应设置以避免冗余播报。
aria-hidden="true"- 正确示例:
<Button><PlusIcon aria-hidden="true" /> Add item</Button> - 另一种正确情况:Lucide图标可能默认设置——标记前请检查
aria-hidden
- 正确示例:
§4 Semantic HTML & Regression Guard
§4 语义化HTML与回归防护
The codebase currently has no anti-patterns. This section guards against regressions.
<div onClick>-
Interactive elements must use native interactive HTML:for actions,
<button>/<a>for navigation. NOT<Link>,<div>, or<span>with<p>.onClick- Flag any new or
<div onClick>in the diff as CRITICAL<span onClick> - Exception: Components from Radix that render proper elements under the hood are fine
- Flag any new
-
Tables must use semantic HTML:,
<table>,<thead>,<tbody>,<th>. The codebase already does this. Flag any new data display that should be a table but uses<td>grid instead.<div>- Consider: elements should have
<th>orscope="col"for complex tablesscope="row"
- Consider:
-
Don't disable zoom: Flagor
user-scalable=noin viewport meta tags.maximum-scale=1
本代码库目前没有反模式。本节用于防止回归。
<div onClick>-
交互元素必须使用原生交互HTML:操作使用,导航使用
<button>/<a>。不能使用带<Link>的onClick、<div>或<span>。<p>- 将差异中的任何新或
<div onClick>标记为CRITICAL(严重)<span onClick> - 例外:Radix组件在底层会渲染正确的元素,这类情况没问题
- 将差异中的任何新
-
表格必须使用语义化HTML:使用、
<table>、<thead>、<tbody>、<th>。本代码库已遵循此规范。标记任何应使用表格但却用<td>网格实现的新数据展示。<div>- 注意:复杂表格中的元素应设置
<th>或scope="col"scope="row"
- 注意:复杂表格中的
-
不要禁用缩放:标记视口元标签中的或
user-scalable=no。maximum-scale=1
§5 Focus Management
§5 焦点管理
Radix Dialog handles focus trap and restore automatically. This section covers what Radix doesn't handle.
-
Custom modals/overlays must manage focus: Any modal-like UI NOT built on Radix Dialog (e.g., custom overlays, fullscreen panels, React Flow side panels) must:
- Move focus into the overlay when it opens
- Trap focus while open (Tab cycles within the overlay)
- Return focus to the trigger when closed
- Close on Escape
-
Focus visible indicator must not be removed:/
outline-nonewithout aoutline: nonereplacement removes the only visual cue for keyboard users.focus-visible:ring-*- Note: The codebase consistently uses alongside
focus-visible:ring-*— this is correct. Only flag if a new component usesoutline-nonewithout the replacement.outline-none
- Note: The codebase consistently uses
-
Route change focus (Next.js App Router): After client-side navigation, focus should move to the main content. Next.js App Router may handle this — only flag if a custom route change mechanism bypasses the framework's handling.
-
Positive tabIndex is an anti-pattern:and
tabIndex={0}are fine.tabIndex={-1}or higher overrides natural order and creates unpredictable navigation. Flag any positive tabIndex values.tabIndex={1}
Radix Dialog会自动处理焦点捕获与恢复。本节涵盖Radix未处理的情况。
-
自定义模态框/浮层必须管理焦点:任何未基于Radix Dialog构建的类模态框UI(如自定义浮层、全屏面板、React Flow侧边面板)必须:
- 打开时将焦点移至浮层内
- 打开时捕获焦点(Tab键在浮层内循环)
- 关闭时将焦点返回至触发器
- 按Escape键可关闭
-
不能移除焦点可见指示器:/
outline-none如果没有outline: none替代方案,会移除键盘用户唯一的视觉定位提示。focus-visible:ring-*- 注意:本代码库始终在旁使用
outline-none——这是正确的。仅当新组件使用focus-visible:ring-*而无替代方案时才标记。outline-none
- 注意:本代码库始终在
-
路由变更焦点(Next.js App Router):客户端导航后,焦点应移至主要内容。Next.js App Router可能已处理此问题——仅当自定义路由变更机制绕过框架处理时才标记。
-
正tabIndex是反模式:和
tabIndex={0}是没问题的。tabIndex={-1}或更高值会覆盖自然顺序,导致不可预测的导航。标记任何正tabIndex值。tabIndex={1}
§6 Dynamic Content & Live Regions
§6 动态内容与实时区域
With 287 toast usages (Sonner) and chat streaming interfaces, announcements for screen readers matter.
-
Sonner toasts: Sonner useswith
role="status"by default. This is correct. Only flag if:aria-live="polite"- A custom toast/notification bypasses Sonner and doesn't use a live region
- An error toast should use (assertive) instead of
role="alert"(polite) for critical errorsrole="status"
-
Loading states should be communicated: Skeleton loaders and spinners should be accompanied by screen reader announcements. Options:
- on the loading container
aria-busy="true" - inside the spinner
<span className="sr-only">Loading...</span> - region that announces "Loading..." then announces when content is ready
aria-live="polite" - Note: The codebase's component already has
Spinner— check that new loading patterns follow suitaria-label
-
Chat streaming messages: For the copilot/playground chat interfaces, new messages should be announced to screen readers. Thelibrary should handle this — only flag if custom chat rendering bypasses the library's announcements.
@inkeep/agents-ui -
Inline form validation: When validation errors appear dynamically (without page reload), they should either:
- Be associated with the input via (shadcn/ui's
aria-describedbydoes this)<FormMessage> - Or use to announce the error
aria-live="polite" - Only flag custom validation rendering outside the pattern
<FormMessage>
- Be associated with the input via
由于有287次Sonner toast使用场景和聊天流界面,屏幕阅读器的播报非常重要。
-
Sonner toast:Sonner默认使用和
role="status"。这是正确的。仅在以下情况标记:aria-live="polite"- 自定义toast/通知绕过Sonner且未使用实时区域
- 错误toast对于严重错误应使用(主动)而非
role="alert"(礼貌)role="status"
-
加载状态应告知用户:骨架加载器和加载 spinner应伴随屏幕阅读器播报。可选方案:
- 在加载容器上设置
aria-busy="true" - 在spinner内添加
<span className="sr-only">Loading...</span> - 使用区域播报"Loading...",然后在内容就绪时播报完成
aria-live="polite" - 注意:本代码库的组件已设置
Spinner——确保新的加载模式遵循此规范aria-label
- 在加载容器上设置
-
聊天流消息:对于copilot/playground聊天界面,新消息应向屏幕阅读器播报。库应已处理此问题——仅当自定义聊天渲染绕过库的播报功能时才标记。
@inkeep/agents-ui -
内联表单验证:当验证错误动态出现(无页面刷新)时,应:
- 通过与输入关联(shadcn/ui的
aria-describedby已实现此功能)<FormMessage> - 或使用播报错误
aria-live="polite" - 仅标记模式外的自定义验证渲染
<FormMessage>
- 通过
§7 Specialized Components
§7 特殊组件
These components have unique a11y considerations beyond standard patterns.
-
Monaco Editor: Has known a11y limitations for screen reader users. When Monaco is used for required input (not just optional code editing), consider providing an alternative text input fallback. Flag only if a new Monaco instance is introduced without consideration.
-
React Flow (node graph editor): Keyboard navigation in visual node editors is inherently difficult. When React Flow is used:
- Ensure all node operations are also accessible via context menus or keyboard shortcuts
- Node labels should be readable by screen readers
- Flag only if new React Flow interactions are added without keyboard alternatives
-
Data tables with actions: Tables with row-level action buttons (common in this codebase) should ensure action buttons have accessible names and the table structure allows screen reader navigation.
- Flag: New table action buttons that are icon-only without
aria-label
- Flag: New table action buttons that are icon-only without
这些组件除标准模式外还有独特的可访问性注意事项。
-
Monaco Editor:已知对屏幕阅读器用户存在可访问性限制。当Monaco用于必填输入(而非可选代码编辑)时,应考虑提供替代文本输入回退。仅当引入新的Monaco实例而未考虑此问题时才标记。
-
React Flow(节点图编辑器):可视化节点编辑器中的键盘导航本质上存在难度。使用React Flow时:
- 确保所有节点操作也可通过上下文菜单或键盘快捷键访问
- 节点标签应能被屏幕阅读器读取
- 仅当添加新的React Flow交互而无键盘替代方案时才标记
-
带操作的数据表:本代码库中常见的带行级操作按钮的数据表,应确保操作按钮有可访问名称,且表结构允许屏幕阅读器导航。
- 标记:仅含图标且无或
aria-label文本的新表格操作按钮sr-only
- 标记:仅含图标且无
Severity Calibration
严重程度校准
| Finding | Severity | Rationale |
|---|---|---|
| CRITICAL | Completely blocks keyboard/screen reader users |
| Keyboard trap (user cannot Tab out of a component) | CRITICAL | Completely blocks keyboard users |
| Custom modal without focus management (not using Radix Dialog) | MAJOR | Major disorientation for keyboard/screen reader users |
| Form input without accessible name (no label, no aria-label) | MAJOR | Screen reader users cannot identify the input |
Icon-only button without | MAJOR | Screen reader users cannot identify the action |
| Dialog without DialogTitle and no aria-label | MAJOR | Screen reader users don't know what the dialog is for |
| MAJOR | Creates ghost focus for screen reader users |
| Error message not associated with input (outside FormMessage) | MAJOR | Screen reader users don't know about validation errors |
| MAJOR | Keyboard users lose their place |
| Radix keyboard handling overridden via stopPropagation | MAJOR | Breaks built-in a11y of the component library |
| Missing alt text on informational image | MINOR | Information not conveyed, but usually not blocking |
Decorative icon missing | MINOR | Redundant announcement — annoying, not blocking |
| Custom notification/toast without live region | MINOR | Status not announced, but visually evident |
| Redundant ARIA on native elements | MINOR | Noise, not breakage — indicates misunderstanding |
Missing | INFO | Navigation degraded in complex tables, not blocking |
| 发现问题 | 严重程度 | 理由 |
|---|---|---|
| CRITICAL | 完全阻止键盘/屏幕阅读器用户访问 |
| 键盘陷阱(用户无法通过Tab键离开组件) | CRITICAL | 完全阻止键盘用户操作 |
| 无焦点管理的自定义模态框(未使用Radix Dialog) | MAJOR | 对键盘/屏幕阅读器用户造成严重方向混乱 |
| 无可访问名称的表单输入(无标签,无aria-label) | MAJOR | 屏幕阅读器用户无法识别输入项 |
无 | MAJOR | 屏幕阅读器用户无法识别操作 |
| 无DialogTitle且无aria-label的对话框 | MAJOR | 屏幕阅读器用户不知道对话框用途 |
含可聚焦子元素的容器设置 | MAJOR | 给屏幕阅读器用户造成幽灵焦点 |
| 未与输入关联的错误消息(FormMessage外) | MAJOR | 屏幕阅读器用户不知道验证错误 |
无 | MAJOR | 键盘用户丢失定位 |
| 通过stopPropagation覆盖Radix键盘处理 | MAJOR | 破坏组件库的内置可访问性 |
| 信息图片缺少alt文本 | MINOR | 信息未传达,但通常不会阻止访问 |
装饰性图标缺少 | MINOR | 冗余播报——烦人但不阻止访问 |
| 无实时区域的自定义通知/toast | MINOR | 状态未播报,但视觉上可见 |
| 原生元素上的冗余ARIA | MINOR | 冗余,无破坏——表示存在误解 |
复杂表格中 | INFO | 复杂表格中导航体验下降,但不阻止访问 |