mcpField
R
radix-ui

radix-ui

github.com/dralgorhythm设计与 UI12856 次安装14 周前更新
代码仓库

关于 radix-ui

面向 React Web 开发的无样式、可访问 UI 基元,内置键盘导航、焦点管理和 ARIA 属性。该技能提供 Dialog、Select 等组件的组合、样式与可访问性实践。

主要功能

  1. 01提供无样式 React UI 基元
  2. 02内置键盘导航与焦点管理
  3. 03自动添加 ARIA 属性
  4. 04涵盖 Dialog、Select 和 Tooltip
  5. 05提供 Tailwind 样式与测试建议

适用场景

  1. 01为 Web 应用构建对话框
  2. 02实现可键盘操作的下拉选择
  3. 03检查交互组件的屏幕阅读器支持

MCP 相关

README 指示使用 Context7 MCP 的 `resolve-library-id` 和 `query-docs` 查询完整 API 参考、组件属性及示例。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dralgorhythm/claude-agentic-framework --skill radix-ui

安装后在 Claude Code 中输入/radix-ui确认已加载。

详细介绍

Radix UI

平台: 仅限 Web。关于移动端模态框/底部弹层,请参阅 expo-sdk 和 react-native-patterns 技能。

如需完整 API 参考、所有组件属性和更多示例,请使用 Context7 MCP(resolve-library-id,然后使用 query-docs)。

概述

适用于 React 的无样式、易访问 UI 基元,内置键盘导航、焦点管理和 ARIA 属性。专为与 Tailwind CSS 和 Framer Motion 组合使用而设计。

版本:最新版本(单独软件包)或统一软件包 radix-ui

安装(单独软件包):

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

安装(统一软件包):

pnpm add radix-ui

统一软件包 radix-ui 捆绑了所有基元,可用于简化依赖项管理。

工作流

添加 Dialog:

  1. 安装:pnpm add @radix-ui/react-dialog
  2. 导入 Dialog 部件:Root、Trigger、Portal、Overlay、Content
  3. 将 Overlay 和 Content 包装在 Portal 中,以确保正确的堆叠顺序
  4. 使用 Tailwind 和 data-[state=] 选择器设置样式
  5. 测试键盘导航(按 Esc 关闭,Tab 焦点限制在模态框内)
  6. 根据需要添加 Framer Motion 动画

添加 Select:

  1. 安装:pnpm add @radix-ui/react-select
  2. 导入 Select 部件:Root、Trigger、Portal、Content、Item
  3. 在 Trigger 中添加 Icon 和 Value,以提供视觉反馈
  4. 使用 data-[state=open] 设置打开/关闭状态的样式
  5. 测试键盘操作(方向键、Enter、输入联想)
  6. 确保 Portal 的 z-index 设置正确

添加工具提示:

  1. 安装:pnpm add @radix-ui/react-tooltip
  2. 使用 TooltipProvider 包装应用(在根节点包装一次)
  3. 为每个工具提示组合 Trigger 和 Content
  4. 设置 delayDuration,控制悬停延迟
  5. 验证屏幕阅读器播报

Dialog(模态框)

import * as Dialog from '@radix-ui/react-dialog';
// OR: import { Dialog } from 'radix-ui';

function ModalExample() {
  const [open, setOpen] = useState(false);

  return (
    <Dialog.Root open={open} onOpenChange={setOpen}>
      <Dialog.Trigger asChild>
        <button className="px-4 py-2 bg-blue-600 text-white rounded">Open Dialog</button>
      </Dialog.Trigger>

      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />

        <Dialog.Content className="fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] max-h-[85vh] w-[90vw] max-w-[500px] rounded-lg bg-white p-6 shadow-lg">
          {/* Title and Description are required for accessibility */}
          <Dialog.Title className="text-lg font-semibold mb-2">Dialog Title</Dialog.Title>
          <Dialog.Description className="text-sm text-gray-600 mb-4">
            Screen readers announce this description.
          </Dialog.Description>

          <div className="flex justify-end gap-2 mt-6">
            <Dialog.Close asChild>
              <button className="px-4 py-2 border rounded">Cancel</button>
            </Dialog.Close>
            <button className="px-4 py-2 bg-blue-600 text-white rounded">Save</button>
          </div>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

Framer Motion 动画:在 Portal 上使用 forceMount,并用 AnimatePresence 包装。将 asChild 传给 Overlay/Content,并使用 motion.div 作为子元素。

Select(下拉菜单)

import * as Select from '@radix-ui/react-select';
import { ChevronDownIcon, CheckIcon } from '@radix-ui/react-icons';

function SelectExample() {
  return (
    <Select.Root defaultValue="apple">
      <Select.Trigger className="inline-flex items-center justify-between rounded px-4 py-2 text-sm bg-white border gap-2 data-[placeholder]:text-gray-400 min-w-[200px]">
        <Select.Value placeholder="Select a fruit..." />
        <Select.Icon><ChevronDownIcon /></Select.Icon>
      </Select.Trigger>

      <Select.Portal>
        <Select.Content className="overflow-hidden bg-white rounded-md shadow-lg border">
          <Select.Viewport className="p-1">
            <Select.Item value="apple" className="relative flex items-center px-8 py-2 rounded text-sm hover:bg-blue-50 outline-none cursor-pointer data-[disabled]:opacity-50">
              <Select.ItemIndicator className="absolute left-2"><CheckIcon /></Select.ItemIndicator>
              <Select.ItemText>Apple</Select.ItemText>
            </Select.Item>
            <Select.Item value="banana" className="relative flex items-center px-8 py-2 rounded text-sm hover:bg-blue-50 outline-none cursor-pointer">
              <Select.ItemIndicator className="absolute left-2"><CheckIcon /></Select.ItemIndicator>
              <Select.ItemText>Banana</Select.ItemText>
            </Select.Item>
          </Select.Viewport>
        </Select.Content>
      </Select.Portal>
    </Select.Root>
  );
}

使用 Select.Group + Select.Label 对选项分组,使用 Select.Separator 添加分隔线。

使用 Tailwind 设置样式

常用数据属性选择器
// Open/closed state
className="data-[state=open]:bg-blue-50 data-[state=closed]:bg-gray-50"

// Disabled
className="data-[disabled]:opacity-50 data-[disabled]:pointer-events-none"

// Checked (checkboxes, radio items)
className="data-[state=checked]:bg-blue-600"

// Side-based positioning
className="data-[side=top]:animate-slide-down data-[side=bottom]:animate-slide-up"
常见布局模式
// Focus ring (required for keyboard UX)
className="outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"

// Centered modal
className="fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%]"

// Backdrop overlay
className="fixed inset-0 bg-black/50 backdrop-blur-sm"

易访问性

键盘导航(自动处理):

  • Dialog:按 Esc 关闭,Tab 焦点限制在模态框内
  • Select:方向键、按 Enter 选择、输入联想搜索
  • DropdownMenu:方向键、按 Enter 选择
  • Tabs:方向键、Home/End 跳转到第一个/最后一个选项卡
  • Tooltip:聚焦 Trigger 时显示工具提示

ARIA(自动添加):

  • Dialog:role="dialog"、aria-labelledby(Title)、aria-describedby(Description)
  • Select:role="combobox"、aria-expanded、aria-controls

始终提供 Dialog.Title 和 Dialog.Description——屏幕阅读器依赖这些内容。

自定义焦点管理:

<Dialog.Content
  onOpenAutoFocus={(e) => { e.preventDefault(); customRef.current?.focus(); }}
  onCloseAutoFocus={(e) => { e.preventDefault(); triggerRef.current?.focus(); }}
>

受控与非受控

// Uncontrolled — component manages its own state
<Dialog.Root defaultOpen={false}>

// Controlled — parent manages state (preferred for complex UIs)
const [open, setOpen] = useState(false);
<Dialog.Root open={open} onOpenChange={setOpen}>

Portal 用法

始终将覆盖层和下拉菜单渲染到 Portal 中,以避免 z-index 冲突、被 overflow: hidden 裁切以及 CSS transform 导致的问题:

<Dialog.Portal>          {/* renders at document.body */}
  <Dialog.Overlay />
  <Dialog.Content />
</Dialog.Portal>

{/* Custom container */}
<Dialog.Portal container={customContainerRef.current}>

最佳实践

  • 使用 asChild 属性与自定义元素组合,避免添加包装 div
  • 始终使用 Portal 渲染覆盖层和下拉菜单,避免 z-index 问题
  • 为所有 Dialog 提供 Title 和 Description(易访问性要求)
  • 使用 data-[state=] 选择器设置打开/关闭状态的样式
  • 复杂状态管理时优先使用受控模式
  • 在应用根节点仅使用一个 TooltipProvider,不要为每个工具提示单独添加
  • 测试每个交互组件的键盘导航

反模式

  • ❌ 忘记使用 Dialog.Portal(会导致 z-index 问题)
  • ❌ 缺少 Dialog.Title 或 Dialog.Description(不符合易访问性要求)
  • ❌ 自定义触发器未使用 asChild(会创建包装 div)
  • ❌ 使用多个 TooltipProvider 实例(造成不必要的开销)
  • ❌ 使用受控模式但未提供 onOpenChange/onValueChange
  • ❌ 混用受控和非受控模式
  • ❌ 忘记添加 focus:ring 样式(键盘操作体验不佳)
  • ❌ 未测试键盘导航

反馈循环

无障碍测试:

# Test with keyboard only (no mouse)
# Tab through all interactive elements
# Esc should close Dialogs, Dropdowns, Selects
# Arrow keys should navigate menus and selects

屏幕阅读器测试:

# macOS: VoiceOver (Cmd+F5)
# Verify Dialog.Title and Dialog.Description are announced
# Check for proper role attributes

视觉回归:测试所有状态:关闭与打开、悬停与聚焦、选中与未选中、禁用,以及不同的视口尺寸。

Framer Motion:使用 forceMount + AnimatePresence。测试动画下焦点管理是否仍然有效。

评论

支持 Markdown