shadcn/ui 组件体系
shadcn/ui 不是一个 npm 包——它是一套可复制的组件源码。你拥有每一行代码,可以自由修改。配合 Tailwind CSS v4、Radix UI 无障碍基础、React Hook Form + Zod 表单校验,它是 Next.js 项目最推荐的 UI 方案。
1. 安装与配置
1.1 初始化
# 在 Next.js 项目中初始化 shadcn/ui
pnpm dlx shadcn@latest init初始化向导会询问:
✔ Which style would you like to use? › New York
✔ Which color would you like to use as base color? › Zinc
✔ Would you like to use CSS variables for colors? › yes
生成的文件:
├── components.json # shadcn/ui 配置文件
├── components/ui/ # 组件源码目录
├── lib/utils.ts # cn() 工具函数
└── app/globals.css # CSS 变量定义
1.2 添加组件
# 按需添加组件(源码复制到 components/ui/)
pnpm dlx shadcn@latest add button
pnpm dlx shadcn@latest add dialog
pnpm dlx shadcn@latest add form
pnpm dlx shadcn@latest add input
pnpm dlx shadcn@latest add select
pnpm dlx shadcn@latest add toast
pnpm dlx shadcn@latest add dropdown-menu
pnpm dlx shadcn@latest add sheet
# 批量添加
pnpm dlx shadcn@latest add button dialog form input select toast1.3 核心工具:cn()
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// 使用
<div className={cn(
'rounded-lg border p-4', // 基础样式
isActive && 'border-primary', // 条件样式
className, // 外部传入的样式
)} />2. 主题定制
2.1 CSS 变量体系
/* app/globals.css */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--destructive: 0 84.2% 60.2%;
--border: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
/* ... */
}
}2.2 品牌色定制
/* AI SaaS 品牌定制示例 */
:root {
--primary: 262 83% 58%; /* 紫色品牌色 */
--primary-foreground: 0 0% 100%;
--ring: 262 83% 58%;
}
.dark {
--primary: 262 83% 68%; /* 暗色模式稍亮 */
--primary-foreground: 0 0% 100%;
}2.3 暗色模式
pnpm add next-themes// components/theme-provider.tsx
'use client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<NextThemesProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</NextThemesProvider>
)
}// components/theme-toggle.tsx
'use client'
import { useTheme } from 'next-themes'
import { Moon, Sun } from 'lucide-react'
import { Button } from '@/components/ui/button'
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
)
}3. 常用组件模式
3.1 确认对话框
// components/confirm-dialog.tsx
'use client'
import {
AlertDialog, AlertDialogAction, AlertDialogCancel,
AlertDialogContent, AlertDialogDescription,
AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
export function ConfirmDialog({
trigger,
title,
description,
onConfirm,
destructive = false,
}: {
trigger: React.ReactNode
title: string
description: string
onConfirm: () => void
destructive?: boolean
}) {
return (
<AlertDialog>
<AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction
onClick={onConfirm}
className={destructive ? 'bg-destructive text-destructive-foreground' : ''}
>
确认
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
// 使用
<ConfirmDialog
trigger={<Button variant="destructive" size="sm">删除</Button>}
title="确定删除这个对话?"
description="删除后无法恢复。"
onConfirm={() => deleteChat(chatId)}
destructive
/>3.2 Toast 通知
// 使用 sonner(shadcn/ui 推荐的 toast 方案)
pnpm dlx shadcn@latest add sonner// app/layout.tsx
import { Toaster } from '@/components/ui/sonner'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Toaster />
</body>
</html>
)
}
// 任何 Client Component 中使用
import { toast } from 'sonner'
toast.success('对话已创建')
toast.error('创建失败,请重试')
toast.loading('正在生成回复...')3.3 响应式侧边栏
// components/app-sidebar.tsx
'use client'
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
import { Button } from '@/components/ui/button'
import { Menu } from 'lucide-react'
export function AppSidebar({ children }: { children: React.ReactNode }) {
return (
<>
{/* 桌面端:固定侧边栏 */}
<aside className="hidden md:flex md:w-64 md:flex-col border-r">
{children}
</aside>
{/* 移动端:抽屉式侧边栏 */}
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden">
<Menu className="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-64 p-0">
{children}
</SheetContent>
</Sheet>
</>
)
}4. 表单系统(React Hook Form + Zod)
4.1 安装
pnpm dlx shadcn@latest add form input select textarea
pnpm add react-hook-form @hookform/resolvers zod4.2 完整表单示例
// components/chat-settings-form.tsx
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import {
Form, FormControl, FormDescription,
FormField, FormItem, FormLabel, FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { toast } from 'sonner'
const formSchema = z.object({
title: z.string().min(1, '标题不能为空').max(100),
model: z.enum(['gpt-4o', 'claude-3.5-sonnet', 'gemini-pro']),
systemPrompt: z.string().max(2000).optional(),
temperature: z.coerce.number().min(0).max(2),
})
type FormValues = z.infer<typeof formSchema>
export function ChatSettingsForm({ onSubmit }: { onSubmit: (data: FormValues) => Promise<void> }) {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
title: '',
model: 'gpt-4o',
systemPrompt: '',
temperature: 0.7,
},
})
async function handleSubmit(data: FormValues) {
try {
await onSubmit(data)
toast.success('设置已保存')
form.reset()
} catch {
toast.error('保存失败')
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>对话标题</FormLabel>
<FormControl>
<Input placeholder="输入对话标题" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="model"
render={({ field }) => (
<FormItem>
<FormLabel>模型</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="选择模型" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="gpt-4o">GPT-4o</SelectItem>
<SelectItem value="claude-3.5-sonnet">Claude 3.5 Sonnet</SelectItem>
<SelectItem value="gemini-pro">Gemini Pro</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="systemPrompt"
render={({ field }) => (
<FormItem>
<FormLabel>System Prompt</FormLabel>
<FormControl>
<Textarea rows={4} placeholder="你是一个..." {...field} />
</FormControl>
<FormDescription>可选,用于设定 AI 的角色和行为。</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="temperature"
render={({ field }) => (
<FormItem>
<FormLabel>Temperature: {field.value}</FormLabel>
<FormControl>
<Input type="range" min="0" max="2" step="0.1" {...field} />
</FormControl>
<FormDescription>越高越创造性,越低越确定性。</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? '保存中...' : '保存设置'}
</Button>
</form>
</Form>
)
}5. 组件定制
5.1 修改源码
shadcn/ui 的组件源码在你的项目中——直接修改即可:
// components/ui/button.tsx — 修改按钮的默认样式
const buttonVariants = cva(
'inline-flex items-center justify-center ...',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
// 新增:AI 品牌渐变按钮
gradient: 'bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:opacity-90',
},
// ...
},
},
)5.2 组合组件
// components/ui/loading-button.tsx
import { Button, type ButtonProps } from '@/components/ui/button'
import { Loader2 } from 'lucide-react'
interface LoadingButtonProps extends ButtonProps {
loading?: boolean
}
export function LoadingButton({ loading, children, disabled, ...props }: LoadingButtonProps) {
return (
<Button disabled={disabled || loading} {...props}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{children}
</Button>
)
}本章小结
- shadcn/ui 是复制源码模式——你拥有完整控制权
- CSS 变量:通过修改
globals.css的 HSL 变量实现品牌定制 - 暗色模式:
next-themes+attribute="class"一行切换 - 表单系统:
React Hook Form+Zod+ shadcn Form 组件 = 类型安全的完整表单 - 组件定制:直接修改源码或组合新组件,无需 override 或 CSS hack
下一章讲 Layout 与模板系统。