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()
})
})
资源
- 官方文档: https://ui.shadcn.com
- Radix UI: https://www.radix-ui.com
- 示例: https://ui.shadcn.com/examples
- 主题: https://ui.shadcn.com/themes
2025年11月说明
截至2025年11月,ShadCN UI是React组件库的行业标准。所有新的Quetrex应用程序必须使用ShadCN UI以确保一致性、无障碍性和可维护性。