frontend-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>
{/* Portal在DOM层级外渲染 */}
<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:带上下文的复合组件
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 - 打开模态框时焦点被捕获在内部
- 按下Esc键可关闭对话框
- 关闭对话框后焦点返回至触发元素
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 /> {/* 在Dialog中嵌套Radix Combobox */} </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
问题:按Esc键无法关闭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}>
{/* 不要阻止Esc键的默认行为 */}
</Dialog.Root>Problem: Dropdown menu positioning is off
问题:下拉菜单定位错误
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 dialogshadcn与原生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 Docs
- 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 Documentation - 变体管理
Examples
示例
- Radix Playground
- shadcn/ui Source - Production examples
- Radix Playground
- shadcn/ui Source - 生产级示例
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提供行为,你赋予它美观。无障碍访问内置其中,定制能力不受限制。