Use Cases (更新: 2026/6/2)

用 Claude Code 和 Radix UI 构建可访问 React UI

用 Claude Code 和 Radix UI 实现 Dialog、Dropdown、Tabs,包含安装、代码、样式、坑点和检查清单。

用 Claude Code 和 Radix UI 构建可访问 React UI

在 React 中自己写弹窗、下拉菜单和标签页,看起来并不难。真正容易出问题的是键盘操作、焦点回到哪里、屏幕阅读器能不能读出标题、Escape 是否能关闭,以及移动端是否溢出。只用鼠标测试时一切正常,发布后才发现键盘用户无法完成操作,这是很多 UI 项目的常见问题。

Radix UI 适合解决这类问题。它不是带完整视觉风格的组件库,而是一组无样式的 UI primitives,也就是“行为地基”。你仍然自己决定颜色、间距、圆角和动效,但 Dialog、Dropdown Menu、Tabs 等复杂交互不必从零实现。

Claude Code 也更适合在这种边界清楚的任务里工作。不要让它凭空写一个 modal,而是明确要求“使用 Radix UI,保留可访问性语义,并接入现有样式”。如果你想使用更高层的 Radix 封装,可以继续读 Claude Code shadcn/ui 指南。如果想系统检查无障碍质量,可以配合 Claude Code 无障碍实现指南

为什么 Radix UI 适合交给 Claude Code

Radix 官方把 Primitives 描述为重视 accessibility、customization 和 developer experience 的低层 UI 组件库。换句话说,Radix 不替你设计品牌视觉,而是提供角色、焦点管理、键盘交互和组件结构。

Dialog 是典型例子。Radix Dialog 支持 modal 和 non-modal,modal 模式会把焦点限制在弹窗内部,支持 Escape 关闭,并通过 TitleDescription 帮助屏幕阅读器理解内容。Tabs 遵循 WAI-ARIA tabs pattern,处理方向键、HomeEnd。Dropdown Menu 提供 Label、Separator、RadioItem 等结构,避免你手写整套菜单焦点逻辑。

Claude Code 的工作循环是先收集上下文,再修改代码,再验证结果。如果让它从 div 开始写复杂交互,生成代码会变长,review 成本也会升高。指定 Radix UI 后,Claude Code 可以专注在项目相关部分:状态、API、样式类名、埋点、测试和检查清单。

flowchart LR
  A["把 UI 需求交给 Claude Code"] --> B["用 Radix UI 负责交互行为"]
  B --> C["连接 React 状态和业务逻辑"]
  C --> D["套用项目 CSS 或设计 token"]
  D --> E["检查键盘、读屏和移动端"]

安装命令

Radix 现在也推荐使用统一的 radix-ui 包,但许多 React 项目仍然使用独立包。本文用独立包,因为在 package.json 中更容易看出项目实际用了哪些 primitives。

npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-tabs

使用 pnpm 时:

pnpm add @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-tabs

如果还没有安装 Claude Code,请先看官方 getting started 文档。npm 安装方式如下:

npm install -g @anthropic-ai/claude-code

团队环境不要只把它当成一个全局 npm 包。需要提前确定升级方式、权限边界、CI 中是否允许运行,以及谁负责 review 生成的 UI 差异。

给 Claude Code 的提示词

好的提示词要描述交互约束,而不是只描述外观。

claude "在现有 React + TypeScript 页面中加入确认 Dialog、用户 Dropdown Menu 和设置 Tabs。
要求:
- 使用 @radix-ui/react-dialog、@radix-ui/react-dropdown-menu、@radix-ui/react-tabs
- Dialog 必须包含 Dialog.Title 和 Dialog.Description
- 只有图标的关闭按钮必须有 aria-label
- 不要移除可见 focus 样式,使用 :focus-visible
- 尽量复用现有 design token
- 完成后列出键盘、移动端、typecheck 和无障碍检查项"

生成后不要只看截图。要看 asChild 是否造成按钮嵌套按钮,Dialog.Title 是否为了视觉效果被删掉,CSS 是否把 focus outline 直接隐藏。

可复制的 React 示例

下面的例子把确认弹窗、用户菜单、设置标签页放在一个文件中。Vite、普通 React SPA、Next.js Client Component 都能使用。Next.js App Router 中请在文件顶部加上 "use client";

import * as React from "react";
import * as Dialog from "@radix-ui/react-dialog";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as Tabs from "@radix-ui/react-tabs";
import "./radix-accessible-demo.css";

type User = { name: string; email: string };

type ConfirmDialogProps = {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  description: string;
  confirmLabel?: string;
  danger?: boolean;
  onConfirm: () => Promise<void> | void;
};

export function ConfirmDialog({
  open,
  onOpenChange,
  title,
  description,
  confirmLabel = "Confirm",
  danger = false,
  onConfirm,
}: ConfirmDialogProps) {
  const [pending, setPending] = React.useState(false);

  async function handleConfirm() {
    setPending(true);
    try {
      await onConfirm();
      onOpenChange(false);
    } finally {
      setPending(false);
    }
  }

  return (
    <Dialog.Root open={open} onOpenChange={onOpenChange}>
      <Dialog.Portal>
        <Dialog.Overlay className="radix-overlay" />
        <Dialog.Content className="radix-dialog">
          <Dialog.Title className="radix-dialog-title">{title}</Dialog.Title>
          <Dialog.Description className="radix-dialog-description">
            {description}
          </Dialog.Description>
          <div className="button-row">
            <Dialog.Close asChild>
              <button type="button" className="button secondary">Cancel</button>
            </Dialog.Close>
            <button
              type="button"
              className={`button ${danger ? "danger" : "primary"}`}
              disabled={pending}
              onClick={handleConfirm}
            >
              {pending ? "Working..." : confirmLabel}
            </button>
          </div>
          <Dialog.Close asChild>
            <button type="button" className="icon-button" aria-label="Close dialog">
              x
            </button>
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

export function UserMenu({
  user,
  onOpenProfile,
  onOpenBilling,
  onSignOut,
}: {
  user: User;
  onOpenProfile: () => void;
  onOpenBilling: () => void;
  onSignOut: () => void;
}) {
  const [theme, setTheme] = React.useState<"light" | "dark" | "system">("system");

  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <button type="button" className="user-trigger" aria-label={`${user.name} menu`}>
          <span className="avatar" aria-hidden="true">{user.name.slice(0, 1)}</span>
          <span>{user.name}</span>
        </button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Portal>
        <DropdownMenu.Content className="dropdown-content" align="end" sideOffset={8}>
          <DropdownMenu.Label className="dropdown-label">{user.email}</DropdownMenu.Label>
          <DropdownMenu.Separator className="dropdown-separator" />
          <DropdownMenu.Item className="dropdown-item" onSelect={() => onOpenProfile()}>
            Profile
          </DropdownMenu.Item>
          <DropdownMenu.Item className="dropdown-item" onSelect={() => onOpenBilling()}>
            Billing
          </DropdownMenu.Item>
          <DropdownMenu.Separator className="dropdown-separator" />
          <DropdownMenu.RadioGroup
            value={theme}
            onValueChange={(value) => setTheme(value as "light" | "dark" | "system")}
          >
            <DropdownMenu.RadioItem className="dropdown-item" value="light">Light</DropdownMenu.RadioItem>
            <DropdownMenu.RadioItem className="dropdown-item" value="dark">Dark</DropdownMenu.RadioItem>
            <DropdownMenu.RadioItem className="dropdown-item" value="system">System</DropdownMenu.RadioItem>
          </DropdownMenu.RadioGroup>
          <DropdownMenu.Separator className="dropdown-separator" />
          <DropdownMenu.Item className="dropdown-item danger-text" onSelect={() => onSignOut()}>
            Sign out
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  );
}

export function SettingsTabs() {
  return (
    <Tabs.Root defaultValue="profile" className="tabs-root">
      <Tabs.List className="tabs-list" aria-label="Account settings">
        <Tabs.Trigger className="tabs-trigger" value="profile">Profile</Tabs.Trigger>
        <Tabs.Trigger className="tabs-trigger" value="security">Security</Tabs.Trigger>
        <Tabs.Trigger className="tabs-trigger" value="notifications">Notifications</Tabs.Trigger>
      </Tabs.List>
      <Tabs.Content className="tabs-content" value="profile">
        <label className="field">
          <span>Display name</span>
          <input defaultValue="Masa" />
        </label>
      </Tabs.Content>
      <Tabs.Content className="tabs-content" value="security">
        <p>Require two-factor authentication before changing billing settings.</p>
        <button type="button" className="button secondary">Review security</button>
      </Tabs.Content>
      <Tabs.Content className="tabs-content" value="notifications">
        <label className="check-row">
          <input type="checkbox" defaultChecked />
          <span>Email me when a project is exported.</span>
        </label>
      </Tabs.Content>
    </Tabs.Root>
  );
}

样式要点

Radix 不带样式,所以你可以使用 Tailwind、CSS Modules、普通 CSS 或 design token。无论用哪种方式,都不要隐藏 focus 样式,也不要把 Dialog 写成固定宽度。

.radix-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.55);
}

.radix-dialog {
  position: fixed;
  left: 50%;
  top: 50%;
  width: min(calc(100vw - 32px), 480px);
  max-height: calc(100vh - 32px);
  overflow: auto;
  transform: translate(-50%, -50%);
  border-radius: 8px;
  background: white;
  box-shadow: 0 24px 80px rgba(15, 23, 42, 0.28);
  padding: 24px;
}

.button-row {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  margin-top: 24px;
}

.dropdown-content {
  min-width: 220px;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  background: white;
  box-shadow: 0 18px 50px rgba(15, 23, 42, 0.18);
  padding: 6px;
}

.dropdown-item {
  border-radius: 6px;
  cursor: pointer;
  outline: none;
  padding: 8px 10px;
}

.dropdown-item[data-highlighted] {
  background: #eff6ff;
  color: #1d4ed8;
}

.tabs-list {
  display: flex;
  border-bottom: 1px solid #e2e8f0;
  gap: 4px;
}

.tabs-trigger {
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  cursor: pointer;
  padding: 10px 12px;
}

.tabs-trigger[data-state="active"] {
  border-color: #2563eb;
  color: #1d4ed8;
  font-weight: 700;
}

:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 2px;
}

3 个实际用例

用例Radix UI 的价值Claude Code 负责的部分
删除项目、取消订阅等确认 Dialog焦点控制、标题和描述更可靠API 调用、pending 状态、错误信息、测试
账号菜单键盘移动、分隔线、单选项更容易维护用户信息、退出登录、计费入口、埋点
设置页面 Tabstablist、tab、tabpanel 的关系更稳定表单拆分、保存逻辑、URL 同步、未保存提示

第一个场景是 SaaS 后台。删除、计费、权限变更都属于高风险操作,最重要的不是动画,而是让用户清楚知道后果。Radix Dialog 给出可靠的行为基础,Claude Code 再接入业务 API。

第二个场景是课程、媒体或会员网站。账号菜单经常包含资料、订单、下载、学习进度和退出登录。自己写 click-only menu 很容易忽略键盘用户,Radix Dropdown Menu 能减少这类问题。

第三个场景是设置页。Tabs 能把 profile、security、notifications 分开,但保存范围必须清楚。可以让 Claude Code 同时添加 dirty state、保存按钮位置和错误提示。

常见坑

第一个坑是为了视觉简洁删掉 Dialog.Title。如果页面上不想显示标题,也要保留给屏幕阅读器使用的标题。MDN 的 dialog role 文档也强调,dialog 需要正确 label 和 focus management。

第二个坑是把 focus outline 全部删掉。可以替换成符合设计系统的 :focus-visible,但不能让键盘用户看不到当前位置。

第三个坑是错误使用 asChild。如果生成结果里出现 button 套 button,HTML 就不合法,浏览器和辅助技术的行为都可能不稳定。

第四个坑是做太多层 modal。WAI-ARIA Modal Dialog Pattern 说明了打开时焦点进入弹窗、关闭后回到触发元素的基本预期。多层弹窗会让这个流程变得难以理解。

第五个坑是只看桌面。移动端固定宽度 Dialog、靠右菜单溢出、Tabs 文案换行后难点按,都会影响真实转化。

官方链接和检查清单

发布前至少检查这些点:只用键盘能打开和关闭 Dialog;Escape 可关闭;关闭后焦点回到触发按钮;Dropdown 能用方向键移动;Tabs 能用方向键切换;手机宽度不溢出。

变现 CTA

这个主题适合连接到实际项目服务,因为读者通常不是只想了解库,而是想把现有 UI 改到可上线、可 review。ClaudeCodeLab 可以通过 Claude Code 培训与咨询 帮团队整理 CLAUDE.md、组件规则、无障碍检查、review prompt 和验证脚本。个人开发者可以先用模板和 checklist,团队则更适合带真实仓库做一次 UI review。

实际测试结果

Masa 在一个 React 测试页面中把手写 modal 替换为 Radix Dialog,并把用户菜单和设置 Tabs 也迁移到 Radix。代码量略有增加,但 review 变得更明确:焦点是否回到触发按钮、标题是否能被读出、手机宽度是否能点击关闭按钮。让 Claude Code 针对这些点复查,比只要求“让 UI 更好看”有用得多。结论是,Radix UI 不是让你跳过无障碍思考的捷径,而是给 Claude Code 一个更安全的行为地基。

#Claude Code #Radix UI #React #无障碍 #UI组件
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。