关于 radix-ui
面向 React Web 开发的无样式、可访问 UI 基元,内置键盘导航、焦点管理和 ARIA 属性。该技能提供 Dialog、Select 等组件的组合、样式与可访问性实践。
主要功能
- 01提供无样式 React UI 基元
- 02内置键盘导航与焦点管理
- 03自动添加 ARIA 属性
- 04涵盖 Dialog、Select 和 Tooltip
- 05提供 Tailwind 样式与测试建议
适用场景
- 01为 Web 应用构建对话框
- 02实现可键盘操作的下拉选择
- 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:
- 安装:
pnpm add @radix-ui/react-dialog - 导入 Dialog 部件:Root、Trigger、Portal、Overlay、Content
- 将 Overlay 和 Content 包装在 Portal 中,以确保正确的堆叠顺序
- 使用 Tailwind 和 data-[state=] 选择器设置样式
- 测试键盘导航(按 Esc 关闭,Tab 焦点限制在模态框内)
- 根据需要添加 Framer Motion 动画
添加 Select:
- 安装:
pnpm add @radix-ui/react-select - 导入 Select 部件:Root、Trigger、Portal、Content、Item
- 在 Trigger 中添加 Icon 和 Value,以提供视觉反馈
- 使用 data-[state=open] 设置打开/关闭状态的样式
- 测试键盘操作(方向键、Enter、输入联想)
- 确保 Portal 的 z-index 设置正确
添加工具提示:
- 安装:
pnpm add @radix-ui/react-tooltip - 使用 TooltipProvider 包装应用(在根节点包装一次)
- 为每个工具提示组合 Trigger 和 Content
- 设置 delayDuration,控制悬停延迟
- 验证屏幕阅读器播报
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。测试动画下焦点管理是否仍然有效。