radix-ui-design-system
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRadix UI Design System
Radix UI 设计系统
Build production-ready, accessible design systems using Radix UI primitives with full customization control and zero style opinions.
使用Radix UI原语构建可投入生产的无障碍设计系统,拥有完全的定制控制权且无默认样式限制。
Overview
概述
Radix UI provides unstyled, accessible components (primitives) that you can customize to match any design system. This skill guides you through building scalable component libraries with Radix UI, focusing on accessibility-first design, theming architecture, and composable patterns.
Key Strengths:
- Headless by design: Full styling control without fighting defaults
- Accessibility built-in: WAI-ARIA compliant, keyboard navigation, screen reader support
- Composable primitives: Build complex components from simple building blocks
- Framework agnostic: Works with React, but styles work anywhere
Radix UI提供无样式、无障碍的组件(原语),你可以对其进行定制以匹配任意设计系统。本技能将指导你使用Radix UI构建可扩展的组件库,重点关注无障碍优先设计、主题架构和可组合模式。
核心优势:
- 原生无头设计:完全掌控样式,无需与默认样式作斗争
- 内置无障碍支持:符合WAI-ARIA标准,支持键盘导航和屏幕阅读器
- 可组合原语:通过简单的构建块构建复杂组件
- 框架无关:与React兼容,但样式可在任意环境中使用
When to Use This Skill
何时使用本技能
- Creating a custom design system from scratch
- Building accessible UI component libraries
- Implementing complex interactive components (Dialog, Dropdown, Tabs, etc.)
- Migrating from styled component libraries to unstyled primitives
- Setting up theming systems with CSS variables or Tailwind
- Need full control over component behavior and styling
- Building applications requiring WCAG 2.1 AA/AAA compliance
- 从零开始创建自定义设计系统
- 构建无障碍UI组件库
- 实现复杂的交互式组件(Dialog、Dropdown、Tabs等)
- 从样式化组件库迁移到无样式原语
- 使用CSS变量或Tailwind搭建主题系统
- 需要完全控制组件行为和样式
- 构建需要符合WCAG 2.1 AA/AAA标准的应用
Do not use this skill when
何时不使用本技能
- You need pre-styled components out of the box (use shadcn/ui, Mantine, etc.)
- Building simple static pages without interactivity
- The project doesn't use React 16.8+ (Radix requires hooks)
- You need components for frameworks other than React
- 你需要开箱即用的预样式化组件(使用shadcn/ui、Mantine等)
- 构建无交互的简单静态页面
- 项目未使用React 16.8+(Radix需要Hooks支持)
- 你需要面向React以外框架的组件
Core Principles
核心原则
1. Accessibility First
1. 无障碍优先
Every Radix primitive is built with accessibility as the foundation:
- Keyboard Navigation: Full keyboard support (Tab, Arrow keys, Enter, Escape)
- Screen Readers: Proper ARIA attributes and live regions
- Focus Management: Automatic focus trapping and restoration
- Disabled States: Proper handling of disabled and aria-disabled
Rule: Never override accessibility features. Enhance, don't replace.
每个Radix原语都以无障碍为基础构建:
- 键盘导航:完整的键盘支持(Tab、方向键、Enter、Escape)
- 屏幕阅读器:正确的ARIA属性和实时区域
- 焦点管理:自动焦点捕获和恢复
- 禁用状态:正确处理disabled和aria-disabled状态
规则:永远不要覆盖无障碍功能。可以增强,但不要替换。
2. Headless Architecture
2. 无头架构
Radix provides behavior, you provide appearance:
tsx
// ❌ Don't fight pre-styled components
<Button className="override-everything" />
// ✅ Radix gives you behavior, you add styling
<Dialog.Root>
<Dialog.Trigger className="your-button-styles" />
<Dialog.Content className="your-modal-styles" />
</Dialog.Root>Radix提供行为,你负责提供外观:
tsx
// ❌ 不要与预样式化组件作斗争
<Button className="override-everything" />
// ✅ Radix提供行为,你添加样式
<Dialog.Root>
<Dialog.Trigger className="your-button-styles" />
<Dialog.Content className="your-modal-styles" />
</Dialog.Root>3. Composition Over Configuration
3. 组合优于配置
Build complex components from simple primitives:
tsx
// Primitive components compose naturally
<Tabs.Root>
<Tabs.List>
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1">Content 1</Tabs.Content>
<Tabs.Content value="tab2">Content 2</Tabs.Content>
</Tabs.Root>通过简单原语构建复杂组件:
tsx
// 原语组件可自然组合
<Tabs.Root>
<Tabs.List>
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1">Content 1</Tabs.Content>
<Tabs.Content value="tab2">Content 2</Tabs.Content>
</Tabs.Root>Getting Started
快速开始
Installation
安装
bash
undefinedbash
undefinedInstall individual primitives (recommended)
推荐:安装单个原语
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu
Or install multiple at once
或一次性安装多个
npm install @radix-ui/react-{dialog,dropdown-menu,tabs,tooltip}
npm install @radix-ui/react-{dialog,dropdown-menu,tabs,tooltip}
For styling (optional but common)
样式相关(可选但常用)
npm install clsx tailwind-merge class-variance-authority
undefinednpm install clsx tailwind-merge class-variance-authority
undefinedBasic Component Pattern
基础组件模式
Every Radix component follows this pattern:
tsx
import * as Dialog from '@radix-ui/react-dialog';
export function MyDialog() {
return (
<Dialog.Root>
{/* Trigger the dialog */}
<Dialog.Trigger asChild>
<button className="trigger-styles">Open</button>
</Dialog.Trigger>
{/* Portal renders outside DOM hierarchy */}
<Dialog.Portal>
{/* Overlay (backdrop) */}
<Dialog.Overlay className="overlay-styles" />
{/* Content (modal) */}
<Dialog.Content className="content-styles">
<Dialog.Title>Title</Dialog.Title>
<Dialog.Description>Description</Dialog.Description>
{/* Your content here */}
<Dialog.Close asChild>
<button>Close</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}每个Radix组件都遵循以下模式:
tsx
import * as Dialog from '@radix-ui/react-dialog';
export function MyDialog() {
return (
<Dialog.Root>
{/* 触发对话框 */}
<Dialog.Trigger asChild>
<button className="trigger-styles">打开</button>
</Dialog.Trigger>
{/* 挂载到DOM层级外部的Portal */}
<Dialog.Portal>
{/* 遮罩层(背景) */}
<Dialog.Overlay className="overlay-styles" />
{/* 内容(模态框) */}
<Dialog.Content className="content-styles">
<Dialog.Title>标题</Dialog.Title>
<Dialog.Description>描述</Dialog.Description>
{/* 你的内容 */}
<Dialog.Close asChild>
<button>关闭</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}Theming Strategies
主题策略
Strategy 1: CSS Variables (Framework-Agnostic)
策略1:CSS变量(框架无关)
Best for: Maximum portability, SSR-friendly
css
/* globals.css */
:root {
--color-primary: 220 90% 56%;
--color-surface: 0 0% 100%;
--radius-base: 0.5rem;
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
[data-theme="dark"] {
--color-primary: 220 90% 66%;
--color-surface: 222 47% 11%;
}tsx
// Component.tsx
<Dialog.Content
className="
bg-[hsl(var(--color-surface))]
rounded-[var(--radius-base)]
shadow-[var(--shadow-lg)]
"
/>最适合:最大可移植性、SSR友好
css
/* globals.css */
:root {
--color-primary: 220 90% 56%;
--color-surface: 0 0% 100%;
--radius-base: 0.5rem;
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
[data-theme="dark"] {
--color-primary: 220 90% 66%;
--color-surface: 222 47% 11%;
}tsx
// Component.tsx
<Dialog.Content
className="
bg-[hsl(var(--color-surface))]
rounded-[var(--radius-base)]
shadow-[var(--shadow-lg)]
"
/>Strategy 2: Tailwind + CVA (Class Variance Authority)
策略2:Tailwind + CVA(Class Variance Authority)
Best for: Tailwind projects, variant-heavy components
tsx
// button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
// Base styles
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
interface ButtonProps extends VariantProps<typeof buttonVariants> {
children: React.ReactNode;
}
export function Button({ variant, size, children }: ButtonProps) {
return (
<button className={cn(buttonVariants({ variant, size }))}>
{children}
</button>
);
}最适合:Tailwind项目、多变体组件
tsx
// button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
// 基础样式
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
interface ButtonProps extends VariantProps<typeof buttonVariants> {
children: React.ReactNode;
}
export function Button({ variant, size, children }: ButtonProps) {
return (
<button className={cn(buttonVariants({ variant, size }))}>
{children}
</button>
);
}Strategy 3: Stitches (CSS-in-JS)
策略3:Stitches(CSS-in-JS)
Best for: Runtime theming, scoped styles
tsx
import { styled } from '@stitches/react';
import * as Dialog from '@radix-ui/react-dialog';
const StyledContent = styled(Dialog.Content, {
backgroundColor: '$surface',
borderRadius: '$md',
padding: '$6',
variants: {
size: {
small: { width: '300px' },
medium: { width: '500px' },
large: { width: '700px' },
},
},
defaultVariants: {
size: 'medium',
},
});最适合:运行时主题、作用域样式
tsx
import { styled } from '@stitches/react';
import * as Dialog from '@radix-ui/react-dialog';
const StyledContent = styled(Dialog.Content, {
backgroundColor: '$surface',
borderRadius: '$md',
padding: '$6',
variants: {
size: {
small: { width: '300px' },
medium: { width: '500px' },
large: { width: '700px' },
},
},
defaultVariants: {
size: 'medium',
},
});Component Patterns
组件模式
Pattern 1: Compound Components with Context
模式1:带Context的复合组件
Use case: Share state between primitive parts
tsx
// Select.tsx
import * as Select from '@radix-ui/react-select';
import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons';
export function CustomSelect({ items, placeholder, onValueChange }) {
return (
<Select.Root onValueChange={onValueChange}>
<Select.Trigger className="select-trigger">
<Select.Value placeholder={placeholder} />
<Select.Icon>
<ChevronDownIcon />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="select-content">
<Select.Viewport>
{items.map((item) => (
<Select.Item
key={item.value}
value={item.value}
className="select-item"
>
<Select.ItemText>{item.label}</Select.ItemText>
<Select.ItemIndicator>
<CheckIcon />
</Select.ItemIndicator>
</Select.Item>
))}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
);
}适用场景:在原语部件之间共享状态
tsx
// Select.tsx
import * as Select from '@radix-ui/react-select';
import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons';
export function CustomSelect({ items, placeholder, onValueChange }) {
return (
<Select.Root onValueChange={onValueChange}>
<Select.Trigger className="select-trigger">
<Select.Value placeholder={placeholder} />
<Select.Icon>
<ChevronDownIcon />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="select-content">
<Select.Viewport>
{items.map((item) => (
<Select.Item
key={item.value}
value={item.value}
className="select-item"
>
<Select.ItemText>{item.label}</Select.ItemText>
<Select.ItemIndicator>
<CheckIcon />
</Select.ItemIndicator>
</Select.Item>
))}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
);
}Pattern 2: Polymorphic Components with asChild
asChild模式2:使用asChild
的多态组件
asChildUse case: Render as different elements without losing behavior
tsx
// ✅ Render as Next.js Link but keep Radix behavior
<Dialog.Trigger asChild>
<Link href="/settings">Open Settings</Link>
</Dialog.Trigger>
// ✅ Render as custom component
<DropdownMenu.Item asChild>
<YourCustomButton icon={<Icon />}>Action</YourCustomButton>
</DropdownMenu.Item>Why matters: Prevents nested button/link issues in accessibility tree.
asChild适用场景:渲染为不同元素且不丢失行为
tsx
// ✅ 渲染为Next.js Link但保留Radix行为
<Dialog.Trigger asChild>
<Link href="/settings">打开设置</Link>
</Dialog.Trigger>
// ✅ 渲染为自定义组件
<DropdownMenu.Item asChild>
<YourCustomButton icon={<Icon />}>操作</YourCustomButton>
</DropdownMenu.Item>asChildPattern 3: Controlled vs Uncontrolled
模式3:受控与非受控组件
tsx
// Uncontrolled (Radix manages state)
<Tabs.Root defaultValue="tab1">
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
</Tabs.Root>
// Controlled (You manage state)
const [activeTab, setActiveTab] = useState('tab1');
<Tabs.Root value={activeTab} onValueChange={setActiveTab}>
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
</Tabs.Root>Rule: Use controlled when you need to sync with external state (URL, Redux, etc.).
tsx
// 非受控(Radix管理状态)
<Tabs.Root defaultValue="tab1">
<Tabs.Trigger value="tab1">标签页1</Tabs.Trigger>
</Tabs.Root>
// 受控(你管理状态)
const [activeTab, setActiveTab] = useState('tab1');
<Tabs.Root value={activeTab} onValueChange={setActiveTab}>
<Tabs.Trigger value="tab1">标签页1</Tabs.Trigger>
</Tabs.Root>规则:当需要与外部状态(URL、Redux等)同步时使用受控模式。
Pattern 4: Animation with Framer Motion
模式4:使用Framer Motion实现动画
tsx
import * as Dialog from '@radix-ui/react-dialog';
import { motion, AnimatePresence } from 'framer-motion';
export function AnimatedDialog({ open, onOpenChange }) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal forceMount>
<AnimatePresence>
{open && (
<>
<Dialog.Overlay asChild>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="dialog-overlay"
/>
</Dialog.Overlay>
<Dialog.Content asChild>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="dialog-content"
>
{/* Content */}
</motion.div>
</Dialog.Content>
</>
)}
</AnimatePresence>
</Dialog.Portal>
</Dialog.Root>
);
}tsx
import * as Dialog from '@radix-ui/react-dialog';
import { motion, AnimatePresence } from 'framer-motion';
export function AnimatedDialog({ open, onOpenChange }) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal forceMount>
<AnimatePresence>
{open && (
<>
<Dialog.Overlay asChild>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="dialog-overlay"
/>
</Dialog.Overlay>
<Dialog.Content asChild>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="dialog-content"
>
{/* 内容 */}
</motion.div>
</Dialog.Content>
</>
)}
</AnimatePresence>
</Dialog.Portal>
</Dialog.Root>
);
}Common Primitives Reference
常用原语参考
Dialog (Modal)
Dialog(模态框)
tsx
<Dialog.Root> {/* State container */}
<Dialog.Trigger /> {/* Opens dialog */}
<Dialog.Portal> {/* Renders in portal */}
<Dialog.Overlay /> {/* Backdrop */}
<Dialog.Content> {/* Modal content */}
<Dialog.Title /> {/* Required for a11y */}
<Dialog.Description /> {/* Required for a11y */}
<Dialog.Close /> {/* Closes dialog */}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>tsx
<Dialog.Root> {/* 状态容器 */}
<Dialog.Trigger /> {/* 打开对话框 */}
<Dialog.Portal> {/* 挂载到Portal */}
<Dialog.Overlay /> {/* 背景遮罩 */}
<Dialog.Content> {/* 模态框内容 */}
<Dialog.Title /> {/* 无障碍支持必填 */}
<Dialog.Description /> {/* 无障碍支持必填 */}
<Dialog.Close /> {/* 关闭对话框 */}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>Dropdown Menu
Dropdown Menu(下拉菜单)
tsx
<DropdownMenu.Root>
<DropdownMenu.Trigger />
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item />
<DropdownMenu.Separator />
<DropdownMenu.CheckboxItem />
<DropdownMenu.RadioGroup>
<DropdownMenu.RadioItem />
</DropdownMenu.RadioGroup>
<DropdownMenu.Sub> {/* Nested menus */}
<DropdownMenu.SubTrigger />
<DropdownMenu.SubContent />
</DropdownMenu.Sub>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>tsx
<DropdownMenu.Root>
<DropdownMenu.Trigger />
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item />
<DropdownMenu.Separator />
<DropdownMenu.CheckboxItem />
<DropdownMenu.RadioGroup>
<DropdownMenu.RadioItem />
</DropdownMenu.RadioGroup>
<DropdownMenu.Sub> {/* 嵌套菜单 */}
<DropdownMenu.SubTrigger />
<DropdownMenu.SubContent />
</DropdownMenu.Sub>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>Tabs
Tabs(标签页)
tsx
<Tabs.Root defaultValue="tab1">
<Tabs.List>
<Tabs.Trigger value="tab1" />
<Tabs.Trigger value="tab2" />
</Tabs.List>
<Tabs.Content value="tab1" />
<Tabs.Content value="tab2" />
</Tabs.Root>tsx
<Tabs.Root defaultValue="tab1">
<Tabs.List>
<Tabs.Trigger value="tab1" />
<Tabs.Trigger value="tab2" />
</Tabs.List>
<Tabs.Content value="tab1" />
<Tabs.Content value="tab2" />
</Tabs.Root>Tooltip
Tooltip(提示框)
tsx
<Tooltip.Provider delayDuration={200}>
<Tooltip.Root>
<Tooltip.Trigger />
<Tooltip.Portal>
<Tooltip.Content side="top" align="center">
Tooltip text
<Tooltip.Arrow />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>tsx
<Tooltip.Provider delayDuration={200}>
<Tooltip.Root>
<Tooltip.Trigger />
<Tooltip.Portal>
<Tooltip.Content side="top" align="center">
提示文本
<Tooltip.Arrow />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>Popover
Popover(弹出框)
tsx
<Popover.Root>
<Popover.Trigger />
<Popover.Portal>
<Popover.Content side="bottom" align="start">
Content
<Popover.Arrow />
<Popover.Close />
</Popover.Content>
</Popover.Portal>
</Popover.Root>tsx
<Popover.Root>
<Popover.Trigger />
<Popover.Portal>
<Popover.Content side="bottom" align="start">
内容
<Popover.Arrow />
<Popover.Close />
</Popover.Content>
</Popover.Portal>
</Popover.Root>Accessibility Checklist
无障碍检查清单
Every Component Must Have:
每个组件必须具备:
- Focus Management: Visible focus indicators on all interactive elements
- Keyboard Navigation: Full keyboard support (Tab, Arrows, Enter, Esc)
- ARIA Labels: Meaningful labels for screen readers
- Color Contrast: WCAG AA minimum (4.5:1 for text, 3:1 for UI)
- Error States: Clear error messages with and
aria-invalidaria-describedby - Loading States: Proper during async operations
aria-busy
- 焦点管理:所有交互式元素有可见的焦点指示器
- 键盘导航:完整的键盘支持(Tab、方向键、Enter、Esc)
- ARIA标签:为屏幕阅读器提供有意义的标签
- 颜色对比度:符合WCAG AA最低标准(文本4.5:1,UI元素3:1)
- 错误状态:清晰的错误消息,搭配和
aria-invalidaria-describedby - 加载状态:异步操作期间正确使用
aria-busy
Dialog-Specific:
Dialog特定要求:
- is present (required for screen readers)
Dialog.Title - provides context
Dialog.Description - Focus trapped inside modal when open
- Escape key closes dialog
- Focus returns to trigger on close
- 存在(屏幕阅读器必填)
Dialog.Title - 提供上下文信息
Dialog.Description - 打开模态框时焦点被捕获在内部
- Escape键可关闭对话框
- 关闭时焦点返回触发元素
Dropdown-Specific:
Dropdown特定要求:
- Arrow keys navigate items
- Type-ahead search works
- First/last item wrapping behavior
- Selected state indicated visually and with ARIA
- 方向键可导航菜单项
- 支持输入搜索
- 首尾项循环导航
- 选中状态有视觉和ARIA标识
Best Practices
最佳实践
✅ Do This
✅ 推荐做法
-
Always useto avoid wrapper divs
asChildtsx<Dialog.Trigger asChild> <button>Open</button> </Dialog.Trigger> -
Provide semantic HTMLtsx
<Dialog.Content asChild> <article role="dialog" aria-labelledby="title"> {/* content */} </article> </Dialog.Content> -
Use CSS variables for themingcss
.dialog-content { background: hsl(var(--surface)); color: hsl(var(--on-surface)); } -
Compose primitives for complex componentstsx
function CommandPalette() { return ( <Dialog.Root> <Dialog.Content> <Combobox /> {/* Radix Combobox inside Dialog */} </Dialog.Content> </Dialog.Root> ); }
-
始终使用避免额外的包装div
asChildtsx<Dialog.Trigger asChild> <button>打开</button> </Dialog.Trigger> -
使用语义化HTMLtsx
<Dialog.Content asChild> <article role="dialog" aria-labelledby="title"> {/* 内容 */} </article> </Dialog.Content> -
使用CSS变量实现主题css
.dialog-content { background: hsl(var(--surface)); color: hsl(var(--on-surface)); } -
组合原语构建复杂组件tsx
function CommandPalette() { return ( <Dialog.Root> <Dialog.Content> <Combobox /> {/* Radix Combobox嵌套在Dialog中 */} </Dialog.Content> </Dialog.Root> ); }
❌ Don't Do This
❌ 不推荐做法
-
Don't skip accessibility partstsx
// ❌ Missing Title and Description <Dialog.Content> <div>Content</div> </Dialog.Content> -
Don't fight the primitivestsx
// ❌ Overriding internal behavior <Dialog.Content onClick={(e) => e.stopPropagation()}> -
Don't mix controlled and uncontrolledtsx
// ❌ Inconsistent state management <Tabs.Root defaultValue="tab1" value={activeTab}> -
Don't ignore keyboard navigationtsx
// ❌ Disabling keyboard behavior <DropdownMenu.Item onKeyDown={(e) => e.preventDefault()}>
-
不要跳过无障碍相关部分tsx
// ❌ 缺少Title和Description <Dialog.Content> <div>内容</div> </Dialog.Content> -
不要修改原语的内部行为tsx
// ❌ 阻止内部事件传播 <Dialog.Content onClick={(e) => e.stopPropagation()}> -
不要混合受控和非受控模式tsx
// ❌ 状态管理不一致 <Tabs.Root defaultValue="tab1" value={activeTab}> -
不要忽略键盘导航tsx
// ❌ 禁用键盘行为 <DropdownMenu.Item onKeyDown={(e) => e.preventDefault()}>
Real-World Examples
实际案例
Example 1: Command Palette (Combo Dialog)
案例1:命令面板(组合Dialog)
tsx
import * as Dialog from '@radix-ui/react-dialog';
import { Command } from 'cmdk';
export function CommandPalette() {
const [open, setOpen] = useState(false);
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener('keydown', down);
return () => document.removeEventListener('keydown', down);
}, []);
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<Command>
<Command.Input placeholder="Type a command..." />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Suggestions">
<Command.Item>Calendar</Command.Item>
<Command.Item>Search Emoji</Command.Item>
</Command.Group>
</Command.List>
</Command>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}tsx
import * as Dialog from '@radix-ui/react-dialog';
import { Command } from 'cmdk';
export function CommandPalette() {
const [open, setOpen] = useState(false);
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener('keydown', down);
return () => document.removeEventListener('keydown', down);
}, []);
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<Command>
<Command.Input placeholder="输入命令..." />
<Command.List>
<Command.Empty>未找到结果。</Command.Empty>
<Command.Group heading="建议">
<Command.Item>日历</Command.Item>
<Command.Item>搜索表情</Command.Item>
</Command.Group>
</Command.List>
</Command>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}Example 2: Dropdown Menu with Icons
案例2:带图标的下拉菜单
tsx
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
export function ActionsMenu() {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="icon-button" aria-label="Actions">
<DotsHorizontalIcon />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="dropdown-content" align="end">
<DropdownMenu.Item className="dropdown-item">
Edit
</DropdownMenu.Item>
<DropdownMenu.Item className="dropdown-item">
Duplicate
</DropdownMenu.Item>
<DropdownMenu.Separator className="dropdown-separator" />
<DropdownMenu.Item className="dropdown-item text-red-500">
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}tsx
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
export function ActionsMenu() {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="icon-button" aria-label="操作">
<DotsHorizontalIcon />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="dropdown-content" align="end">
<DropdownMenu.Item className="dropdown-item">
编辑
</DropdownMenu.Item>
<DropdownMenu.Item className="dropdown-item">
复制
</DropdownMenu.Item>
<DropdownMenu.Separator className="dropdown-separator" />
<DropdownMenu.Item className="dropdown-item text-red-500">
删除
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}Example 3: Form with Radix Select + React Hook Form
案例3:Radix Select + React Hook Form表单
tsx
import * as Select from '@radix-ui/react-select';
import { useForm, Controller } from 'react-hook-form';
interface FormData {
country: string;
}
export function CountryForm() {
const { control, handleSubmit } = useForm<FormData>();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="country"
control={control}
render={({ field }) => (
<Select.Root onValueChange={field.onChange} value={field.value}>
<Select.Trigger className="select-trigger">
<Select.Value placeholder="Select a country" />
<Select.Icon />
</Select.Trigger>
<Select.Portal>
<Select.Content className="select-content">
<Select.Viewport>
<Select.Item value="us">United States</Select.Item>
<Select.Item value="ca">Canada</Select.Item>
<Select.Item value="uk">United Kingdom</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
)}
/>
<button type="submit">Submit</button>
</form>
);
}tsx
import * as Select from '@radix-ui/react-select';
import { useForm, Controller } from 'react-hook-form';
interface FormData {
country: string;
}
export function CountryForm() {
const { control, handleSubmit } = useForm<FormData>();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="country"
control={control}
render={({ field }) => (
<Select.Root onValueChange={field.onChange} value={field.value}>
<Select.Trigger className="select-trigger">
<Select.Value placeholder="选择国家" />
<Select.Icon />
</Select.Trigger>
<Select.Portal>
<Select.Content className="select-content">
<Select.Viewport>
<Select.Item value="us">美国</Select.Item>
<Select.Item value="ca">加拿大</Select.Item>
<Select.Item value="uk">英国</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
)}
/>
<button type="submit">提交</button>
</form>
);
}Troubleshooting
故障排除
Problem: Dialog doesn't close on Escape key
问题:按Escape键无法关闭Dialog
Cause: event prevented or state not synced
onEscapeKeyDownopenSolution:
tsx
<Dialog.Root open={open} onOpenChange={setOpen}>
{/* Don't prevent default on escape */}
</Dialog.Root>原因:事件被阻止,或状态未同步
onEscapeKeyDownopen解决方案:
tsx
<Dialog.Root open={open} onOpenChange={setOpen}>
{/* 不要阻止Escape键的默认行为 */}
</Dialog.Root>Problem: Dropdown menu positioning is off
问题:Dropdown菜单定位错误
Cause: Parent container has or transform
overflow: hiddenSolution:
tsx
// Use Portal to render outside overflow container
<DropdownMenu.Portal>
<DropdownMenu.Content />
</DropdownMenu.Portal>原因:父容器设置了或transform
overflow: hidden解决方案:
tsx
// 使用Portal将菜单渲染到溢出容器外部
<DropdownMenu.Portal>
<DropdownMenu.Content />
</DropdownMenu.Portal>Problem: Animations don't work
问题:动画不生效
Cause: Portal content unmounts immediately
Solution:
tsx
// Use forceMount + AnimatePresence
<Dialog.Portal forceMount>
<AnimatePresence>
{open && <Dialog.Content />}
</AnimatePresence>
</Dialog.Portal>原因:Portal内容立即卸载
解决方案:
tsx
// 使用forceMount + AnimatePresence
<Dialog.Portal forceMount>
<AnimatePresence>
{open && <Dialog.Content />}
</AnimatePresence>
</Dialog.Portal>Problem: TypeScript errors with asChild
asChild问题:使用asChild
时出现TypeScript错误
asChildCause: Type inference issues with polymorphic components
Solution:
tsx
// Explicitly type your component
<Dialog.Trigger asChild>
<button type="button">Open</button>
</Dialog.Trigger>原因:多态组件的类型推断问题
解决方案:
tsx
// 显式指定组件类型
<Dialog.Trigger asChild>
<button type="button">打开</button>
</Dialog.Trigger>Performance Optimization
性能优化
1. Code Splitting
1. 代码分割
tsx
// Lazy load heavy primitives
const Dialog = lazy(() => import('@radix-ui/react-dialog'));
const DropdownMenu = lazy(() => import('@radix-ui/react-dropdown-menu'));tsx
// 懒加载体积较大的原语
const Dialog = lazy(() => import('@radix-ui/react-dialog'));
const DropdownMenu = lazy(() => import('@radix-ui/react-dropdown-menu'));2. Portal Container Reuse
2. 复用Portal容器
tsx
// Create portal container once
<Tooltip.Provider>
{/* All tooltips share portal container */}
<Tooltip.Root>...</Tooltip.Root>
<Tooltip.Root>...</Tooltip.Root>
</Tooltip.Provider>tsx
// 一次性创建Portal容器
<Tooltip.Provider>
{/* 所有提示框共享同一个Portal容器 */}
<Tooltip.Root>...</Tooltip.Root>
<Tooltip.Root>...</Tooltip.Root>
</Tooltip.Provider>3. Memoization
3. 记忆化
tsx
// Memoize expensive render functions
const SelectItems = memo(({ items }) => (
items.map((item) => <Select.Item key={item.value} value={item.value} />)
));tsx
// 对昂贵的渲染函数进行记忆化
const SelectItems = memo(({ items }) => (
items.map((item) => <Select.Item key={item.value} value={item.value} />)
));Integration with Popular Tools
与主流工具集成
shadcn/ui (Built on Radix)
shadcn/ui(基于Radix构建)
shadcn/ui is a collection of copy-paste components built with Radix + Tailwind.
bash
npx shadcn-ui@latest init
npx shadcn-ui@latest add dialogWhen to use shadcn vs raw Radix:
- Use shadcn: Quick prototyping, standard designs
- Use raw Radix: Full customization, unique designs
shadcn/ui是一组基于Radix + Tailwind的复制即用型组件。
bash
npx shadcn-ui@latest init
npx shadcn-ui@latest add dialog何时使用shadcn vs 原生Radix:
- 使用shadcn:快速原型开发、标准设计
- 使用原生Radix:完全定制、独特设计
Radix Themes (Official Styled System)
Radix Themes(官方样式系统)
tsx
import { Theme, Button, Dialog } from '@radix-ui/themes';
function App() {
return (
<Theme accentColor="crimson" grayColor="sand">
<Button>Click me</Button>
</Theme>
);
}tsx
import { Theme, Button, Dialog } from '@radix-ui/themes';
function App() {
return (
<Theme accentColor="crimson" grayColor="sand">
<Button>点击我</Button>
</Theme>
);
}Related Skills
相关技能
- - Tailwind + Radix integration patterns
@tailwind-design-system - - React composition patterns
@react-patterns - - Overall frontend architecture
@frontend-design - - WCAG compliance testing
@accessibility-compliance
- - Tailwind + Radix集成模式
@tailwind-design-system - - React组合模式
@react-patterns - - 整体前端架构
@frontend-design - - WCAG合规性测试
@accessibility-compliance
Resources
资源
Official Documentation
官方文档
- Radix UI Docs
- Radix Colors - Accessible color system
- Radix Icons - Icon library
- Radix UI 文档
- Radix Colors - 无障碍色彩系统
- Radix Icons - 图标库
Community Resources
社区资源
- shadcn/ui - Component collection
- Radix UI Discord - Community support
- CVA Documentation - Variant management
- shadcn/ui - 组件集合
- Radix UI Discord - 社区支持
- CVA 文档 - 变体管理
Examples
示例
- Radix Playground
- shadcn/ui Source - Production examples
- Radix Playground
- shadcn/ui 源码 - 生产级示例
Quick Reference
快速参考
Installation
安装
bash
npm install @radix-ui/react-{primitive-name}bash
npm install @radix-ui/react-{primitive-name}Basic Pattern
基础模式
tsx
<Primitive.Root>
<Primitive.Trigger />
<Primitive.Portal>
<Primitive.Content />
</Primitive.Portal>
</Primitive.Root>tsx
<Primitive.Root>
<Primitive.Trigger />
<Primitive.Portal>
<Primitive.Content />
</Primitive.Portal>
</Primitive.Root>Key Props
关键属性
- - Render as child element
asChild - - Uncontrolled default
defaultValue - /
value- Controlled stateonValueChange - /
open- Open stateonOpenChange - /
side- Positioningalign
Remember: Radix gives you behavior, you give it beauty. Accessibility is built-in, customization is unlimited.
- - 渲染为子元素
asChild - - 非受控模式默认值
defaultValue - /
value- 受控模式状态onValueChange - /
open- 打开状态onOpenChange - /
side- 定位align
记住:Radix为你提供行为,你为它赋予美观。无障碍支持内置其中,定制能力不受限制。