frontend-radix-ui-design-system

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Radix 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
undefined
bash
undefined

Install 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
undefined
npm install clsx tailwind-merge class-variance-authority
undefined

Basic 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

模式2:使用
asChild
的多态组件

Use 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
asChild
matters
: Prevents nested button/link issues in accessibility tree.
使用场景:渲染为不同元素且不丢失行为
tsx
// ✅ 渲染为Next.js Link同时保留Radix行为
<Dialog.Trigger asChild>
  <Link href="/settings">打开设置</Link>
</Dialog.Trigger>

// ✅ 渲染为自定义组件
<DropdownMenu.Item asChild>
  <YourCustomButton icon={<Icon />}>操作</YourCustomButton>
</DropdownMenu.Item>
asChild
的重要性
:避免无障碍树中出现嵌套按钮/链接问题。

Pattern 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
    aria-invalid
    and
    aria-describedby
  • Loading States: Proper
    aria-busy
    during async operations
  • 焦点管理:所有交互元素拥有可见焦点指示器
  • 键盘导航:完整键盘支持(Tab、方向键、Enter、Esc)
  • ARIA标签:为屏幕阅读器提供有意义的标签
  • 颜色对比度:符合WCAG AA最低标准(文本4.5:1,UI元素3:1)
  • 错误状态:清晰的错误提示,搭配
    aria-invalid
    aria-describedby
  • 加载状态:异步操作时正确使用
    aria-busy

Dialog-Specific:

Dialog专属检查项:

  • Dialog.Title
    is present (required for screen readers)
  • Dialog.Description
    provides context
  • 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

✅ 推荐做法

  1. Always use
    asChild
    to avoid wrapper divs
    tsx
    <Dialog.Trigger asChild>
      <button>Open</button>
    </Dialog.Trigger>
  2. Provide semantic HTML
    tsx
    <Dialog.Content asChild>
      <article role="dialog" aria-labelledby="title">
        {/* content */}
      </article>
    </Dialog.Content>
  3. Use CSS variables for theming
    css
    .dialog-content {
      background: hsl(var(--surface));
      color: hsl(var(--on-surface));
    }
  4. Compose primitives for complex components
    tsx
    function CommandPalette() {
      return (
        <Dialog.Root>
          <Dialog.Content>
            <Combobox /> {/* Radix Combobox inside Dialog */}
          </Dialog.Content>
        </Dialog.Root>
      );
    }
  1. 始终使用
    asChild
    避免多余包裹div
    tsx
    <Dialog.Trigger asChild>
      <button>打开</button>
    </Dialog.Trigger>
  2. 使用语义化HTML
    tsx
    <Dialog.Content asChild>
      <article role="dialog" aria-labelledby="title">
        {/* 内容 */}
      </article>
    </Dialog.Content>
  3. 使用CSS变量实现主题
    css
    .dialog-content {
      background: hsl(var(--surface));
      color: hsl(var(--on-surface));
    }
  4. 组合原生素材构建复杂组件
    tsx
    function CommandPalette() {
      return (
        <Dialog.Root>
          <Dialog.Content>
            <Combobox /> {/* 在Dialog中嵌套Radix Combobox */}
          </Dialog.Content>
        </Dialog.Root>
      );
    }

❌ Don't Do This

❌ 不推荐做法

  1. Don't skip accessibility parts
    tsx
    // ❌ Missing Title and Description
    <Dialog.Content>
      <div>Content</div>
    </Dialog.Content>
  2. Don't fight the primitives
    tsx
    // ❌ Overriding internal behavior
    <Dialog.Content onClick={(e) => e.stopPropagation()}>
  3. Don't mix controlled and uncontrolled
    tsx
    // ❌ Inconsistent state management
    <Tabs.Root defaultValue="tab1" value={activeTab}>
  4. Don't ignore keyboard navigation
    tsx
    // ❌ Disabling keyboard behavior
    <DropdownMenu.Item onKeyDown={(e) => e.preventDefault()}>

  1. 不要跳过无障碍相关部分
    tsx
    // ❌ 缺少Title与Description
    <Dialog.Content>
      <div>内容</div>
    </Dialog.Content>
  2. 不要与原生素材的内置行为对抗
    tsx
    // ❌ 覆盖内部行为
    <Dialog.Content onClick={(e) => e.stopPropagation()}>
  3. 不要混合受控与非受控模式
    tsx
    // ❌ 状态管理不一致
    <Tabs.Root defaultValue="tab1" value={activeTab}>
  4. 不要忽略键盘导航
    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:
onEscapeKeyDown
event prevented or
open
state not synced
Solution:
tsx
<Dialog.Root open={open} onOpenChange={setOpen}>
  {/* Don't prevent default on escape */}
</Dialog.Root>
原因
onEscapeKeyDown
事件被阻止,或
open
状态未同步
解决方案
tsx
<Dialog.Root open={open} onOpenChange={setOpen}>
  {/* 不要阻止Esc键的默认行为 */}
</Dialog.Root>

Problem: Dropdown menu positioning is off

问题:下拉菜单定位错误

Cause: Parent container has
overflow: hidden
or transform
Solution:
tsx
// Use Portal to render outside overflow container
<DropdownMenu.Portal>
  <DropdownMenu.Content />
</DropdownMenu.Portal>
原因:父容器设置了
overflow: hidden
或transform
解决方案
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
时出现TypeScript错误

Cause: 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 dialog
When 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与原生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-design-system
    - Tailwind + Radix integration patterns
  • @react-patterns
    - React composition patterns
  • @frontend-design
    - Overall frontend architecture
  • @accessibility-compliance
    - WCAG compliance testing

  • @tailwind-design-system
    - Tailwind + Radix集成模式
  • @react-patterns
    - React组合模式
  • @frontend-design
    - 整体前端架构
  • @accessibility-compliance
    - WCAG合规性测试

Resources

资源

Official Documentation

官方文档

Community Resources

社区资源

Examples

示例


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

关键属性

  • asChild
    - Render as child element
  • defaultValue
    - Uncontrolled default
  • value
    /
    onValueChange
    - Controlled state
  • open
    /
    onOpenChange
    - Open state
  • side
    /
    align
    - Positioning

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提供行为,你赋予它美观。无障碍访问内置其中,定制能力不受限制。