ShadCNUI组件开发规范Skill shadcn-ui-patterns

ShadCN UI组件开发规范是一套针对React前端开发的标准化指南,专注于UI组件构建、无障碍访问(WCAG 2.1 AA)、TailwindCSS最佳实践和TypeScript类型安全。该规范详细说明了如何使用ShadCN UI库(基于Radix UI原语)进行现代Web应用开发,包括组件安装模式、表单验证(React Hook Form + Zod)、服务器/客户端组件划分、深色模式支持以及严格的测试方法。适用于构建企业级React应用、Next.js项目,确保代码一致性、可维护性和卓越的用户体验。

前端开发 0 次安装 0 次浏览 更新于 3/2/2026

name: shadcn-ui-patterns description: 在构建UI组件时使用。强制执行ShadCN UI模式、无障碍标准(Radix UI)以及2025年11月的TailwindCSS最佳实践。 allowed-tools: Read, Grep, Glob

ShadCN UI 模式 - 2025年11月标准

何时使用

  • 构建新的UI组件
  • 重构现有组件以使用ShadCN
  • 实现带验证的表单
  • 创建模态框、对话框和覆盖层
  • 确保无障碍合规性

为什么选择ShadCN UI?

  • 复制粘贴,而非npm - 完全拥有组件代码所有权
  • Radix UI 原语 - 内置无障碍功能(符合WCAG 2.1 AA标准)
  • TailwindCSS优先 - 完全可定制,无需CSS-in-JS
  • 原生TypeScript - 类型安全的属性和变体
  • 服务器组件兼容 - 适用于Next.js 15 App Router

核心原则

1. 组件安装模式

# 按需安装单个组件
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add input
npx shadcn@latest add label

组件被复制到 src/components/ui/ 目录 - 您拥有代码所有权。

2. 组件使用模式

按钮组件

import { Button } from "@/components/ui/button"

// ✅ 正确做法:使用语义变体
<Button variant="default">保存</Button>
<Button variant="destructive">删除</Button>
<Button variant="outline">取消</Button>
<Button variant="ghost">跳过</Button>
<Button variant="link">了解更多</Button>

// ✅ 正确做法:使用尺寸变体
<Button size="default">中等</Button>
<Button size="sm">小</Button>
<Button size="lg">大</Button>
<Button size="icon"><Icon /></Button>

// ❌ 错误做法:不使用Button组件创建自定义按钮
<button className="px-4 py-2 bg-blue-500">错误示例</button>

对话框/模态框组件

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

// ✅ 正确做法:使用正确的对话框结构(无障碍)
<Dialog>
  <DialogTrigger asChild>
    <Button>打开设置</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>设置</DialogTitle>
      <DialogDescription>
        在此配置您的应用程序设置。
      </DialogDescription>
    </DialogHeader>
    {/* 对话框内容 */}
  </DialogContent>
</Dialog>

// ❌ 错误做法:跳过DialogHeader或DialogTitle(破坏屏幕阅读器)
<DialogContent>
  <h2>设置</h2> {/* 错误 - 应使用DialogTitle */}
</DialogContent>

表单组件(配合React Hook Form + Zod)

import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"

// ✅ 正确做法:首先定义Zod模式(验证)
const formSchema = z.object({
  email: z.string().email("无效的电子邮件地址"),
  password: z.string().min(8, "密码必须至少8个字符"),
})

function LoginForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      email: "",
      password: "",
    },
  })

  async function onSubmit(values: z.infer<typeof formSchema>) {
    // 类型安全的已验证数据
    console.log(values)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>电子邮件</FormLabel>
              <FormControl>
                <Input placeholder="you@example.com" {...field} />
              </FormControl>
              <FormDescription>
                我们绝不会分享您的电子邮件。
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>密码</FormLabel>
              <FormControl>
                <Input type="password" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <Button type="submit">登录</Button>
      </form>
    </Form>
  )
}

// ❌ 错误做法:使用无验证的非受控表单
<form>
  <input name="email" /> {/* 无验证 */}
</form>

3. 服务器组件 vs 客户端组件

// ✅ 正确做法:对静态对话框使用服务器组件
import { Dialog, DialogContent } from "@/components/ui/dialog"

export default function ServerDialog() {
  // 不需要 'use client'
  return <Dialog>...</Dialog>
}

// ✅ 正确做法:当需要状态时使用客户端组件
'use client'

import { useState } from 'react'
import { Dialog, DialogContent } from "@/components/ui/dialog"

export function ClientDialog() {
  const [open, setOpen] = useState(false)

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogContent>...</DialogContent>
    </Dialog>
  )
}

4. 无障碍要求

焦点管理

// ✅ 正确做法:使用带asChild的DialogTrigger以实现正确的焦点管理
<DialogTrigger asChild>
  <Button>打开</Button>
</DialogTrigger>

// ❌ 错误做法:手动触发而没有正确的焦点处理
<Button onClick={() => setOpen(true)}>打开</Button>

键盘导航

// ✅ ShadCN自动处理以下功能:
// - ESC键关闭对话框
// - Tab键导航可聚焦元素
// - Enter/Space键激活按钮
// - 方向键导航菜单

// ❌ 错误做法:在没有充分理由的情况下覆盖默认键盘行为

屏幕阅读器支持

// ✅ 正确做法:始终包含DialogTitle(ARIA必需)
<DialogHeader>
  <DialogTitle>删除项目</DialogTitle>
  <DialogDescription>
    此操作无法撤销。
  </DialogDescription>
</DialogHeader>

// ❌ 错误做法:错误使用视觉隐藏标题
<DialogTitle className="sr-only">删除</DialogTitle>
// 仅在有清晰的视觉替代方案时才隐藏

5. 常用组件

组件 使用场景 关键属性
Button 所有可点击操作 variant, size, asChild
Dialog 模态框、确认框 open, onOpenChange
Sheet 侧边面板、抽屉 side, open, onOpenChange
Popover 工具提示、菜单 open, onOpenChange
Form 所有表单 form (来自useForm)
Input 文本输入 type, placeholder
Select 下拉菜单 value, onValueChange
Checkbox 布尔输入 checked, onCheckedChange
RadioGroup 单选 value, onValueChange
Table 数据表格 table (来自TanStack Table)
Card 内容容器 CardHeader, CardContent, CardFooter
Toast 通知 title, description, variant
Command 命令面板 onSelect
Tabs 标签导航 value, onValueChange

6. TailwindCSS 最佳实践

// ✅ 正确做法:使用Tailwind工具类
<Button className="w-full mt-4">提交</Button>

// ✅ 正确做法:使用cn()辅助函数处理条件类
import { cn } from "@/lib/utils"

<Button className={cn(
  "w-full",
  isLoading && "opacity-50 cursor-not-allowed"
)}>
  提交
</Button>

// ❌ 错误做法:使用内联样式
<Button style={{ width: '100%', marginTop: '16px' }}>提交</Button>

// ❌ 错误做法:为组件创建自定义CSS文件
// styles.css
.my-button { width: 100%; }

7. 深色模式支持

// ✅ 正确做法:使用Tailwind深色模式类
<div className="bg-white dark:bg-gray-900 text-black dark:text-white">
  内容
</div>

// ✅ ShadCN组件内置深色模式支持
<Button variant="default">
  {/* 自动为深色模式设置样式 */}
</Button>

常见错误

❌ 缺少DialogTitle(无障碍违规)

// 错误
<DialogContent>
  <h2>设置</h2>
  <p>内容</p>
</DialogContent>

// 正确
<DialogContent>
  <DialogHeader>
    <DialogTitle>设置</DialogTitle>
  </DialogHeader>
  <p>内容</p>
</DialogContent>

❌ 表单不使用Form组件

// 错误 - 无验证,用户体验差
<form>
  <input name="email" />
  <button type="submit">提交</button>
</form>

// 正确 - 验证、错误消息、无障碍
<Form {...form}>
  <form onSubmit={form.handleSubmit(onSubmit)}>
    <FormField name="email" ... />
  </form>
</Form>

❌ 硬编码颜色而非使用变体

// 错误
<Button className="bg-red-500 hover:bg-red-600">删除</Button>

// 正确
<Button variant="destructive">删除</Button>

❌ 触发器不使用asChild

// 错误 - 创建不必要的嵌套按钮
<DialogTrigger>
  <Button>打开</Button>
</DialogTrigger>
// 渲染:<button><button>打开</button></button>(无效HTML)

// 正确 - 将属性合并到单个按钮中
<DialogTrigger asChild>
  <Button>打开</Button>
</DialogTrigger>
// 渲染:<button>打开</button>

测试ShadCN组件

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Dialog, DialogTrigger, DialogContent } from '@/components/ui/dialog'

describe('Dialog', () => {
  it('点击触发器时应打开', async () => {
    const user = userEvent.setup()

    render(
      <Dialog>
        <DialogTrigger asChild>
          <button>打开</button>
        </DialogTrigger>
        <DialogContent>
          <div>对话框内容</div>
        </DialogContent>
      </Dialog>
    )

    // 对话框内容初始不应可见
    expect(screen.queryByText('对话框内容')).not.toBeInTheDocument()

    // 点击触发器
    await user.click(screen.getByText('打开'))

    // 对话框内容现在应可见
    expect(screen.getByText('对话框内容')).toBeInTheDocument()
  })

  it('应按ESC键关闭', async () => {
    const user = userEvent.setup()

    render(
      <Dialog defaultOpen>
        <DialogContent>对话框内容</DialogContent>
      </Dialog>
    )

    expect(screen.getByText('对话框内容')).toBeInTheDocument()

    await user.keyboard('{Escape}')

    expect(screen.queryByText('对话框内容')).not.toBeInTheDocument()
  })
})

资源

2025年11月说明

截至2025年11月,ShadCN UI是React组件库的行业标准。所有新的Quetrex应用程序必须使用ShadCN UI以确保一致性、无障碍性和可维护性。