accessibility-checklist

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Accessibility 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: Radix
    Dialog
    and
    Sheet
    require
    DialogTitle
    /
    SheetTitle
    for screen reader announcement. If
    DialogTitle
    is omitted or visually hidden without
    aria-label
    on
    DialogContent
    , screen readers announce an unlabeled dialog.
    • Common violation:
      <DialogContent>
      with no
      <DialogTitle>
      and no
      aria-label
    • Note: Using
      <VisuallyHidden><DialogTitle>...</DialogTitle></VisuallyHidden>
      is a valid pattern for dialogs where a visible title doesn't fit the design
  • AlertDialog without description:
    AlertDialogContent
    should include
    AlertDialogDescription
    for screen readers to understand the confirmation context. If omitted, add
    aria-describedby={undefined}
    to explicitly opt out (otherwise Radix warns).
  • Select/Combobox without accessible trigger label: Radix
    Select
    needs
    aria-label
    on the trigger when there's no visible label. Custom
    generic-select.tsx
    and
    generic-combo-box.tsx
    wrappers should propagate labels.
    • Common violation:
      <Select>
      inside a form field that has a visual label, but the label isn't associated via
      htmlFor
      or wrapping
  • DropdownMenu items without accessible names: Icon-only menu items need text content or
    aria-label
    . Menu items that are just icons (e.g., copy, delete, edit) need text.
    • Correct pattern:
      <DropdownMenuItem><TrashIcon /> Delete</DropdownMenuItem>
      (icon + text)
    • Violation:
      <DropdownMenuItem><TrashIcon /></DropdownMenuItem>
      (icon only, no text, no aria-label)
  • 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 needs
    aria-label
    as well.
    • Common pattern to flag:
      <Tooltip><TooltipTrigger><Button><Icon /></Button></TooltipTrigger><TooltipContent>Delete</TooltipContent></Tooltip>
      — Button needs
      aria-label="Delete"
  • Overriding Radix's keyboard handling: If a component wraps a Radix primitive and adds
    onKeyDown
    that calls
    e.preventDefault()
    or
    e.stopPropagation()
    , it may break Radix's built-in keyboard navigation.

这是针对本代码库信号最强的章节。正确使用Radix时,它能正确处理可访问性问题——错误源于误用。
  • 无标题的Dialog/Sheet:Radix的
    Dialog
    Sheet
    需要
    DialogTitle
    /
    SheetTitle
    供屏幕阅读器播报。如果省略
    DialogTitle
    ,或在
    DialogContent
    上未设置
    aria-label
    就将其视觉隐藏,屏幕阅读器会播报一个未标记的对话框。
    • 常见违规:
      <DialogContent>
      既没有
      <DialogTitle>
      也没有
      aria-label
    • 注意:对于不需要可见标题的对话框,使用
      <VisuallyHidden><DialogTitle>...</DialogTitle></VisuallyHidden>
      是有效的模式
  • 无描述的AlertDialog
    AlertDialogContent
    应包含
    AlertDialogDescription
    ,以便屏幕阅读器理解确认上下文。如果省略,需添加
    aria-describedby={undefined}
    以明确选择退出(否则Radix会发出警告)。
  • 无可访问触发标签的Select/Combobox:当没有可见标签时,Radix的
    Select
    需要在触发器上设置
    aria-label
    。自定义的
    generic-select.tsx
    generic-combo-box.tsx
    包装器应传递标签。
    • 常见违规:
      <Select>
      位于有视觉标签的表单字段内,但标签未通过
      htmlFor
      或包裹方式关联
  • 无可访问名称的DropdownMenu项:仅含图标菜单项需要文本内容或
    aria-label
    。仅由图标组成的菜单项(如复制、删除、编辑)需要添加文本。
    • 正确模式:
      <DropdownMenuItem><TrashIcon /> Delete</DropdownMenuItem>
      (图标+文本)
    • 违规:
      <DropdownMenuItem><TrashIcon /></DropdownMenuItem>
      (仅图标,无文本,无aria-label)
  • 仅用Tooltip作为可访问名称:Tooltip文本并非所有屏幕阅读器都能可靠播报。如果控件的唯一可访问名称在Tooltip中,还需要同时设置
    aria-label
    • 需标记的常见模式:
      <Tooltip><TooltipTrigger><Button><Icon /></Button></TooltipTrigger><TooltipContent>Delete</TooltipContent></Tooltip>
      ——Button需要添加
      aria-label="Delete"
  • 覆盖Radix的键盘处理逻辑:如果组件包装了Radix原语并添加了调用
    e.preventDefault()
    e.stopPropagation()
    onKeyDown
    ,可能会破坏Radix内置的键盘导航功能。

§2 Forms & Labels

§2 表单与标签

The codebase uses react-hook-form + Zod with shadcn/ui's
Form
component, which auto-associates labels via
FormItem
context. Issues arise when forms bypass this pattern.
  • Every form input must have an accessible name: Via
    <FormLabel>
    ,
    <label htmlFor={id}>
    ,
    aria-label
    , or
    aria-labelledby
    . Placeholder text alone is NOT a label.
    • Common violation: Custom inputs outside
      <FormField>
      /
      <FormItem>
      that don't get auto-association
    • Common violation:
      <Input placeholder="Enter name" />
      used standalone without any label
  • Error messages must be associated with their input: shadcn/ui's
    <FormMessage>
    auto-associates via
    aria-describedby
    when inside
    <FormItem>
    . Custom error rendering outside this pattern loses the association.
    • Flag: Error text rendered near an input but not using
      <FormMessage>
      or manual
      aria-describedby
  • Required fields must be indicated programmatically: Use
    aria-required="true"
    or native
    required
    , not just a visual asterisk. The
    Form
    component doesn't add this automatically — it comes from the Zod schema validation at submit time, not at the HTML level.
  • Grouped controls need group semantics: Radio groups and checkbox groups should use
    <RadioGroup>
    (Radix) or
    <fieldset>
    /
    <legend>
    . Loose radio buttons or checkboxes without group context confuse screen readers.
    • Scope: Configuration pages, settings forms, multi-option selectors

本代码库使用react-hook-form + Zod搭配shadcn/ui的
Form
组件,该组件会通过
FormItem
上下文自动关联标签。当表单绕过此模式时会出现问题。
  • 每个表单输入必须有可访问名称:通过
    <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
    属性,不能仅靠视觉星号。
    Form
    组件不会自动添加此属性——它来自提交时的Zod模式验证,而非HTML层面。
  • 分组控件需要分组语义:单选按钮组和复选框组应使用
    <RadioGroup>
    (Radix)或
    <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
    aria-label
    : Buttons containing only an icon (no visible text) need
    aria-label
    describing the action.
    • Common violation:
      <Button variant="ghost" size="icon"><TrashIcon /></Button>
      without
      aria-label
    • Very common in: data tables (row actions), toolbars, card headers, dialog close buttons
    • Note: shadcn/ui's
      Dialog
      close button already includes
      <span className="sr-only">Close</span>
      — don't flag this
  • Icon-only links need accessible names: Same as buttons —
    <a>
    or
    <Link>
    with only an icon needs
    aria-label
    .
  • sr-only
    text is a valid alternative to
    aria-label
    :
    <Button><TrashIcon /><span className="sr-only">Delete item</span></Button>
    is correct. Don't flag this pattern as missing a label.
  • Decorative icons should be hidden: Icons that are purely decorative (next to visible text) should have
    aria-hidden="true"
    to avoid redundant announcements.
    • Correct:
      <Button><PlusIcon aria-hidden="true" /> Add item</Button>
    • Also correct: Lucide icons may set
      aria-hidden
      by default — check before flagging

由于使用了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
<div onClick>
anti-patterns. This section guards against regressions.
  • Interactive elements must use native interactive HTML:
    <button>
    for actions,
    <a>
    /
    <Link>
    for navigation. NOT
    <div>
    ,
    <span>
    , or
    <p>
    with
    onClick
    .
    • Flag any new
      <div onClick>
      or
      <span onClick>
      in the diff as CRITICAL
    • Exception: Components from Radix that render proper elements under the hood are fine
  • Tables must use semantic HTML:
    <table>
    ,
    <thead>
    ,
    <tbody>
    ,
    <th>
    ,
    <td>
    . The codebase already does this. Flag any new data display that should be a table but uses
    <div>
    grid instead.
    • Consider:
      <th>
      elements should have
      scope="col"
      or
      scope="row"
      for complex tables
  • Don't disable zoom: Flag
    user-scalable=no
    or
    maximum-scale=1
    in viewport meta tags.

本代码库目前没有
<div onClick>
反模式。本节用于防止回归。
  • 交互元素必须使用原生交互HTML:操作使用
    <button>
    ,导航使用
    <a>
    /
    <Link>
    。不能使用带
    onClick
    <div>
    <span>
    <p>
    • 将差异中的任何新
      <div onClick>
      <span onClick>
      标记为CRITICAL(严重)
    • 例外: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-none
    /
    outline: none
    without a
    focus-visible:ring-*
    replacement removes the only visual cue for keyboard users.
    • Note: The codebase consistently uses
      focus-visible:ring-*
      alongside
      outline-none
      — this is correct. Only flag if a new component uses
      outline-none
      without the replacement.
  • 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:
    tabIndex={0}
    and
    tabIndex={-1}
    are fine.
    tabIndex={1}
    or higher overrides natural order and creates unpredictable navigation. Flag any positive tabIndex values.

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={1}
    或更高值会覆盖自然顺序,导致不可预测的导航。标记任何正tabIndex值。

§6 Dynamic Content & Live Regions

§6 动态内容与实时区域

With 287 toast usages (Sonner) and chat streaming interfaces, announcements for screen readers matter.
  • Sonner toasts: Sonner uses
    role="status"
    with
    aria-live="polite"
    by default. This is correct. Only flag if:
    • A custom toast/notification bypasses Sonner and doesn't use a live region
    • An error toast should use
      role="alert"
      (assertive) instead of
      role="status"
      (polite) for critical errors
  • Loading states should be communicated: Skeleton loaders and spinners should be accompanied by screen reader announcements. Options:
    • aria-busy="true"
      on the loading container
    • <span className="sr-only">Loading...</span>
      inside the spinner
    • aria-live="polite"
      region that announces "Loading..." then announces when content is ready
    • Note: The codebase's
      Spinner
      component already has
      aria-label
      — check that new loading patterns follow suit
  • Chat streaming messages: For the copilot/playground chat interfaces, new messages should be announced to screen readers. The
    @inkeep/agents-ui
    library should handle this — only flag if custom chat rendering bypasses the library's announcements.
  • Inline form validation: When validation errors appear dynamically (without page reload), they should either:
    • Be associated with the input via
      aria-describedby
      (shadcn/ui's
      <FormMessage>
      does this)
    • Or use
      aria-live="polite"
      to announce the error
    • Only flag custom validation rendering outside the
      <FormMessage>
      pattern

由于有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>
    • 使用
      aria-live="polite"
      区域播报"Loading...",然后在内容就绪时播报完成
    • 注意:本代码库的
      Spinner
      组件已设置
      aria-label
      ——确保新的加载模式遵循此规范
  • 聊天流消息:对于copilot/playground聊天界面,新消息应向屏幕阅读器播报。
    @inkeep/agents-ui
    库应已处理此问题——仅当自定义聊天渲染绕过库的播报功能时才标记。
  • 内联表单验证:当验证错误动态出现(无页面刷新)时,应:
    • 通过
      aria-describedby
      与输入关联(shadcn/ui的
      <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

这些组件除标准模式外还有独特的可访问性注意事项。
  • Monaco Editor:已知对屏幕阅读器用户存在可访问性限制。当Monaco用于必填输入(而非可选代码编辑)时,应考虑提供替代文本输入回退。仅当引入新的Monaco实例而未考虑此问题时才标记。
  • React Flow(节点图编辑器):可视化节点编辑器中的键盘导航本质上存在难度。使用React Flow时:
    • 确保所有节点操作也可通过上下文菜单或键盘快捷键访问
    • 节点标签应能被屏幕阅读器读取
    • 仅当添加新的React Flow交互而无键盘替代方案时才标记
  • 带操作的数据表:本代码库中常见的带行级操作按钮的数据表,应确保操作按钮有可访问名称,且表结构允许屏幕阅读器导航。
    • 标记:仅含图标且无
      aria-label
      sr-only
      文本的新表格操作按钮

Severity Calibration

严重程度校准

FindingSeverityRationale
<div onClick>
or
<span onClick>
(non-semantic interactive element)
CRITICALCompletely blocks keyboard/screen reader users
Keyboard trap (user cannot Tab out of a component)CRITICALCompletely blocks keyboard users
Custom modal without focus management (not using Radix Dialog)MAJORMajor disorientation for keyboard/screen reader users
Form input without accessible name (no label, no aria-label)MAJORScreen reader users cannot identify the input
Icon-only button without
aria-label
or
sr-only
text
MAJORScreen reader users cannot identify the action
Dialog without DialogTitle and no aria-labelMAJORScreen reader users don't know what the dialog is for
aria-hidden="true"
on container with focusable children
MAJORCreates ghost focus for screen reader users
Error message not associated with input (outside FormMessage)MAJORScreen reader users don't know about validation errors
outline-none
without
focus-visible:ring
replacement
MAJORKeyboard users lose their place
Radix keyboard handling overridden via stopPropagationMAJORBreaks built-in a11y of the component library
Missing alt text on informational imageMINORInformation not conveyed, but usually not blocking
Decorative icon missing
aria-hidden="true"
MINORRedundant announcement — annoying, not blocking
Custom notification/toast without live regionMINORStatus not announced, but visually evident
Redundant ARIA on native elementsMINORNoise, not breakage — indicates misunderstanding
Missing
scope
on
<th>
in complex tables
INFONavigation degraded in complex tables, not blocking
发现问题严重程度理由
<div onClick>
<span onClick>
(非语义化交互元素)
CRITICAL完全阻止键盘/屏幕阅读器用户访问
键盘陷阱(用户无法通过Tab键离开组件)CRITICAL完全阻止键盘用户操作
无焦点管理的自定义模态框(未使用Radix Dialog)MAJOR对键盘/屏幕阅读器用户造成严重方向混乱
无可访问名称的表单输入(无标签,无aria-label)MAJOR屏幕阅读器用户无法识别输入项
aria-label
sr-only
文本的仅图标按钮
MAJOR屏幕阅读器用户无法识别操作
无DialogTitle且无aria-label的对话框MAJOR屏幕阅读器用户不知道对话框用途
含可聚焦子元素的容器设置
aria-hidden="true"
MAJOR给屏幕阅读器用户造成幽灵焦点
未与输入关联的错误消息(FormMessage外)MAJOR屏幕阅读器用户不知道验证错误
focus-visible:ring
替代的
outline-none
MAJOR键盘用户丢失定位
通过stopPropagation覆盖Radix键盘处理MAJOR破坏组件库的内置可访问性
信息图片缺少alt文本MINOR信息未传达,但通常不会阻止访问
装饰性图标缺少
aria-hidden="true"
MINOR冗余播报——烦人但不阻止访问
无实时区域的自定义通知/toastMINOR状态未播报,但视觉上可见
原生元素上的冗余ARIAMINOR冗余,无破坏——表示存在误解
复杂表格中
<th>
缺少
scope
INFO复杂表格中导航体验下降,但不阻止访问