name: shadcn-ui
version: 1.0.0
description: 当使用 shadcn/ui 组件构建 UI、Tailwind CSS 布局、结合 react-hook-form 与 zod 的表单模式、主题化、深色模式、侧边栏布局、移动端导航,或任何关于 shadcn 组件的问题时使用。
triggers:
- shadcn
- shadcn/ui
- radix
- 组件库
- UI 组件
- 表单模式
- react-hook-form
- 深色模式
- 主题化
- 侧边栏布局
- 对话框
- 抽屉
- 通知
- 下拉菜单
- 命令面板
- 数据表格
role: 专家
scope: 实现
output-format: 代码
使用 shadcn/ui、Tailwind CSS、react-hook-form 和 zod 构建生产级 UI 的完整指南。
shadcn/ui 不是一个组件库——它是一套基于 Radix UI 原语构建的、可复制粘贴的组件集合。你拥有这些代码。组件是添加到你的项目中的,而不是作为依赖项安装的。
# 在 Next.js 项目中初始化 shadcn/ui
npx shadcn@latest init
# 添加单个组件
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add input
npx shadcn@latest add select
npx shadcn@latest add table
npx shadcn@latest add toast
npx shadcn@latest add dropdown-menu
npx shadcn@latest add sheet
npx shadcn@latest add tabs
npx shadcn@latest add sidebar
# 一次性添加多个组件
npx shadcn@latest add button card input label textarea select checkbox
| 组件 | 使用场景 |
|---|---|
sidebar |
带有可折叠区块的应用级导航 |
navigation-menu |
带有下拉菜单的顶级网站导航 |
breadcrumb |
显示页面层级/当前位置 |
tabs |
在同一上下文中切换相关视图 |
separator |
内容区块之间的视觉分隔线 |
sheet |
侧滑面板(移动端导航、筛选器、详情视图) |
resizable |
可调整大小的面板布局 |
| 组件 | 使用场景 |
|---|---|
form |
任何需要验证的表单(包装 react-hook-form) |
input |
文本、邮箱、密码、数字输入 |
textarea |
多行文本输入 |
select |
从列表中选择(类原生体验) |
combobox |
可搜索的选择器(使用 command + popover) |
checkbox |
布尔值或多选开关 |
radio-group |
从少量选项中进行单选 |
switch |
开/关切换(设置、偏好) |
slider |
数值范围选择 |
date-picker |
日期选择(使用 calendar + popover) |
toggle |
按下/未按下状态(工具栏按钮) |
| 组件 | 使用场景 |
|---|---|
dialog |
模态确认框、表单或详情视图 |
alert-dialog |
破坏性操作确认(“确定要删除吗?”) |
sheet |
侧边面板,用于表单、筛选器、移动端导航 |
toast |
短暂的非阻塞性通知(通过 sonner) |
alert |
内联状态消息(信息、警告、错误) |
tooltip |
图标/按钮的悬停提示 |
popover |
点击时显示的富内容(颜色选择器、日期选择器) |
hover-card |
悬停时预览内容(用户资料、链接) |
skeleton |
加载占位符 |
progress |
任务完成进度指示器 |
| 组件 | 使用场景 |
|---|---|
table |
表格数据展示 |
data-table |
支持排序、筛选、分页的表格(使用 @tanstack/react-table) |
card |
带有头部、主体、底部的卡片容器 |
badge |
状态标签、徽章、计数 |
avatar |
用户头像 |
accordion |
可折叠的 FAQ 或设置区块 |
carousel |
图片/内容轮播 |
scroll-area |
自定义可滚动容器 |
| 组件 | 使用场景 |
|---|---|
button |
主要操作、表单提交 |
dropdown-menu |
上下文菜单、操作菜单 |
context-menu |
右键菜单 |
menubar |
应用菜单栏 |
command |
命令面板 / 搜索(⌘K) |
npx shadcn@latest add form input select textarea checkbox button
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Checkbox } from '@/components/ui/checkbox'
import { toast } from 'sonner'
const formSchema = z.object({
name: z.string().min(2, '姓名至少需要 2 个字符'),
email: z.string().email('邮箱地址无效'),
role: z.enum(['admin', 'user', 'editor'], { required_error: '请选择一个角色' }),
bio: z.string().max(500).optional(),
notifications: z.boolean().default(false),
})
type FormValues = z.infer<typeof formSchema>
export function UserForm() {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
email: '',
bio: '',
notifications: false,
},
})
async function onSubmit(values: FormValues) {
try {
await createUser(values)
toast.success('用户创建成功')
form.reset()
} catch (error) {
toast.error('用户创建失败')
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>姓名</FormLabel>
<FormControl>
<Input placeholder="张三" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormControl>
<Input type="email" placeholder="zhangsan@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>角色</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="选择一个角色" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">管理员</SelectItem>
<SelectItem value="editor">编辑</SelectItem>
<SelectItem value="user">用户</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>个人简介</FormLabel>
<FormControl>
<Textarea placeholder="介绍一下你自己..." {...field} />
</FormControl>
<FormDescription>最多 500 个字符</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="notifications"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>邮件通知</FormLabel>
<FormDescription>接收关于账户活动的邮件</FormDescription>
</div>
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? '创建中...' : '创建用户'}
</Button>
</form>
</Form>
)
}
'use client'
import { useFormState } from 'react-dom'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
export function ContactForm() {
const form = useForm<FormValues>({
resolver: zodResolver(schema),
})
async function onSubmit(values: FormValues) {
const formData = new FormData()
Object.entries(values).forEach(([key, value]) => formData.append(key, String(value)))
await submitContact(formData)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{/* 字段 */}
</form>
</Form>
)
}
npm install next-themes
npx shadcn@latest add dropdown-menu
// app/providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
</ThemeProvider>
)
}
// components/theme-toggle.tsx
'use client'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
export function ThemeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">切换主题</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>浅色</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>深色</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>跟随系统</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
globals.css 中自定义颜色@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* ... 其他变量 */
}
}
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
import { AppSidebar } from '@/components/app-sidebar'
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider>
<AppSidebar />
<main className="flex-1">
<header className="flex h-14 items-center gap-4 border-b px-6">
<SidebarTrigger />
<h1 className="text-lg font-semibold">仪表盘</h1>
</header>
<div className="p-6">{children}</div>
</main>
</SidebarProvider>
)
}
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
import { Button } from '@/components/ui/button'
import { Menu } from 'lucide-react'
export function Header() {
return (
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">
<div className="container flex h-14 items-center">
<div className="mr-4 hidden md:flex">
<Logo />
<nav className="flex items-center gap-6 text-sm ml-6">
<Link href="/dashboard">仪表盘</Link>
<Link href="/settings">设置</Link>
</nav>
</div>
{/* 移动端汉堡菜单 */}
<Sheet>
<SheetTrigger asChild>
<Button variant="outline" size="icon" className="md:hidden">
<Menu className="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[300px]">
<nav className="flex flex-col gap-4 mt-8">
<Link href="/dashboard">仪表盘</Link>
<Link href="/settings">设置</Link>
</nav>
</SheetContent>
</Sheet>
<div className="flex flex-1 items-center justify-end gap-2">
<ThemeToggle />
<UserMenu />
</div>
</div>
</header>
)
}
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
export function StatsGrid({ stats }: { stats: Stat[] }) {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{stats.map((stat) => (
<Card key={stat.label}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{stat.label}</CardTitle>
<stat.icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stat.value}</div>
<p className="text-xs text-muted-foreground">{stat.description}</p>
</CardContent>
</Card>
))}
</div>
)
}
```tsx
// 居中
非常长的文本...
// 多行截断多行截断文本...
// 宽高比