名称: shadcn-ui 描述: 实现 shadcn/ui 的指南 - 一个使用 Radix UI 和 Tailwind CSS 构建的、设计美观、可访问的 UI 组件集合。在构建用户界面、添加 UI 组件或在基于 React 的应用中实现设计系统时使用。 许可证: MIT 版本: 1.0.0
shadcn/ui 技能
shadcn/ui 是一个基于 TypeScript、Tailwind CSS 和 Radix UI 原语的、设计美观、可访问的组件集合和代码分发平台。它不是传统的组件库,而是一个可重用组件的集合,您可以复制并粘贴到您的应用中。
参考
https://ui.shadcn.com/llms.txt
何时使用此技能
在以下情况下使用此技能:
- 使用基于 React 的框架(如 Next.js、Vite、Remix、Astro 等)构建用户界面
- 向应用添加预构建、可访问的 UI 组件
- 使用 Tailwind CSS 实现设计系统
- 设置带验证的表单(使用 React Hook Form + Zod)
- 添加数据表、图表或复杂的 UI 模式
- 通过一致的主题实现深色模式
- 自定义组件外观和行为
核心概念
关键原则
- 开放代码:将组件复制到您的项目中,自由修改
- 组合性:基于 Radix UI 的可组合原语构建
- 分发:通过 CLI 分发组件,而非 npm 包
- 美观默认值:经过深思熟虑的设计,具有卓越的美学
- AI 就绪:结构化以轻松集成 AI 工具
架构
shadcn/ui 遵循独特的分发模型:
- CLI 工具:通过
npx shadcn@latest安装和管理组件 - 组件注册表:组件的中央存储库
- 本地组件:组件位于您的
components/ui/目录中 - 完全所有权:您拥有代码,按需修改
技术栈
- TypeScript:完整的类型安全
- Tailwind CSS:优先使用实用类的样式(支持 v3 和 v4)
- Radix UI:可访问的、无样式的原语
- Class Variance Authority:组件变体
- React 19:与最新 React 兼容
安装与设置
初始设置
使用 CLI(推荐):
npx shadcn@latest init
CLI 将提示:
- 框架偏好(如 Next.js、Vite 等)
- TypeScript 或 JavaScript
- 组件安装位置
- CSS 变量或 Tailwind 配置
- 颜色主题偏好
- 全局 CSS 文件位置
手动设置:
- 安装依赖:
npm install tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react
- 创建
components.json:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "zinc",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
- 配置 Tailwind:
// tailwind.config.ts
import type { Config } from "tailwindcss"
const config: Config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
],
theme: {
extend: {},
},
plugins: [require("tailwindcss-animate")],
}
export default config
- 创建实用文件:
// lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
添加组件
通过 CLI:
# 添加单个组件
npx shadcn@latest add button
# 添加多个组件
npx shadcn@latest add button card dialog
# 添加所有组件
npx shadcn@latest add --all
添加组件时发生的情况:
- 组件文件被复制到
components/ui/ - 依赖自动安装
- 组件准备好导入和使用
组件类别
表单和输入组件
按钮:
import { Button } from "@/components/ui/button"
<Button variant="default">点击我</Button>
<Button variant="destructive">删除</Button>
<Button variant="outline" size="sm">小号</Button>
<Button variant="ghost" size="icon">
<Icon />
</Button>
输入框:
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
<div>
<Label htmlFor="email">邮箱</Label>
<Input id="email" type="email" placeholder="you@example.com" />
</div>
表单(带验证):
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"
const formSchema = z.object({
username: z.string().min(2).max(50),
email: z.string().email(),
})
function ProfileForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>用户名</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
这是您的公共显示名称。
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">提交</Button>
</form>
</Form>
)
}
选择框:
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="主题" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">浅色</SelectItem>
<SelectItem value="dark">深色</SelectItem>
<SelectItem value="system">系统</SelectItem>
</SelectContent>
</Select>
复选框:
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
<div className="flex items-center space-x-2">
<Checkbox id="terms" />
<Label htmlFor="terms">接受条款和条件</Label>
</div>
日期选择器:
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Button } from "@/components/ui/button"
import { CalendarIcon } from "lucide-react"
import { format } from "date-fns"
const [date, setDate] = useState<Date>()
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : "选择日期"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar mode="single" selected={date} onSelect={setDate} />
</PopoverContent>
</Popover>
布局和导航
卡片:
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
<Card>
<CardHeader>
<CardTitle>卡片标题</CardTitle>
<CardDescription>卡片描述</CardDescription>
</CardHeader>
<CardContent>
<p>卡片内容</p>
</CardContent>
<CardFooter>
<p>卡片页脚</p>
</CardFooter>
</Card>
标签页:
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="account">
<TabsList>
<TabsTrigger value="account">账户</TabsTrigger>
<TabsTrigger value="password">密码</TabsTrigger>
</TabsList>
<TabsContent value="account">账户设置</TabsContent>
<TabsContent value="password">密码设置</TabsContent>
</Tabs>
手风琴:
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>它是可访问的吗?</AccordionTrigger>
<AccordionContent>
是的。它遵循 WAI-ARIA 设计模式。
</AccordionContent>
</AccordionItem>
</Accordion>
导航菜单:
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@/components/ui/navigation-menu"
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>项目一</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink>链接</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
覆盖层和对话框
对话框:
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>
抽屉:
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
<Drawer>
<DrawerTrigger>打开</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>您确定吗?</DrawerTitle>
<DrawerDescription>此操作无法撤销。</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<Button>提交</Button>
<DrawerClose>取消</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
弹出层:
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
<Popover>
<PopoverTrigger>打开</PopoverTrigger>
<PopoverContent>在此处放置内容。</PopoverContent>
</Popover>
提示框:
import { useToast } from "@/hooks/use-toast"
import { Button } from "@/components/ui/button"
const { toast } = useToast()
<Button
onClick={() => {
toast({
title: "已安排:赶上",
description: "2023年2月10日星期五下午5:57",
})
}}
>
显示提示
</Button>
命令框:
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
<Command>
<CommandInput placeholder="输入命令或搜索..." />
<CommandList>
<CommandEmpty>未找到结果。</CommandEmpty>
<CommandGroup heading="建议">
<CommandItem>日历</CommandItem>
<CommandItem>搜索表情符号</CommandItem>
<CommandItem>计算器</CommandItem>
</CommandGroup>
</CommandList>
</Command>
反馈和状态
警告框:
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
<Alert>
<AlertTitle>注意!</AlertTitle>
<AlertDescription>
您可以使用 CLI 向应用添加组件。
</AlertDescription>
</Alert>
<Alert variant="destructive">
<AlertTitle>错误</AlertTitle>
<AlertDescription>
您的会话已过期。请重新登录。
</AlertDescription>
</Alert>
进度条:
import { Progress } from "@/components/ui/progress"
<Progress value={33} />
骨架屏:
import { Skeleton } from "@/components/ui/skeleton"
<div className="flex items-center space-x-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
显示组件
表格:
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
<Table>
<TableCaption>您最近发票的列表。</TableCaption>
<TableHeader>
<TableRow>
<TableHead>发票</TableHead>
<TableHead>状态</TableHead>
<TableHead>金额</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV001</TableCell>
<TableCell>已支付</TableCell>
<TableCell>$250.00</TableCell>
</TableRow>
</TableBody>
</Table>
数据表(带排序/过滤):
npx shadcn@latest add data-table
头像:
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
徽章:
import { Badge } from "@/components/ui/badge"
<Badge>默认</Badge>
<Badge variant="secondary">次要</Badge>
<Badge variant="destructive">破坏性</Badge>
<Badge variant="outline">轮廓</Badge>
主题和自定义
深色模式设置
Next.js(App Router):
- 安装 next-themes:
npm install next-themes
- 创建主题提供者:
// components/theme-provider.tsx
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
- 用提供者包装应用:
// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}
- 添加主题切换:
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ThemeToggle() {
const { setTheme, theme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<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>
)
}
颜色自定义
使用 CSS 变量:
/* 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%;
--input: 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%;
/* ... */
}
}
使用 Tailwind 配置:
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
// ...
},
},
},
}
组件自定义
由于组件位于您的代码库中,您可以直接修改它们:
// components/ui/button.tsx
// 修改变体,添加新变体,更改样式
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
destructive: "bg-destructive text-destructive-foreground",
outline: "border border-input bg-background",
// 添加自定义变体
custom: "bg-gradient-to-r from-purple-500 to-pink-500 text-white",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
// 添加自定义大小
xl: "h-14 rounded-md px-10 text-lg",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
高级模式
服务器操作(Next.js)
// app/actions.ts
"use server"
import { z } from "zod"
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
export async function createUser(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get("email"),
password: formData.get("password"),
})
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// 创建用户
}
// app/signup/page.tsx
import { createUser } from "@/app/actions"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export default function SignupPage() {
return (
<form action={createUser}>
<Input name="email" type="email" />
<Input name="password" type="password" />
<Button type="submit">注册</Button>
</form>
)
}
可重用表单模式
// lib/form-utils.ts
import { UseFormReturn } from "react-hook-form"
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
export function TextFormField({
form,
name,
label,
placeholder,
type = "text",
}: {
form: UseFormReturn<any>
name: string
label: string
placeholder?: string
type?: string
}) {
return (
<FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<Input type={type} placeholder={placeholder} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)
}
响应式组件组合
import { useMediaQuery } from "@/hooks/use-media-query"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { Drawer, DrawerContent } from "@/components/ui/drawer"
export function ResponsiveDialog({ children, ...props }) {
const isDesktop = useMediaQuery("(min-width: 768px)")
if (isDesktop) {
return (
<Dialog {...props}>
<DialogContent>{children}</DialogContent>
</Dialog>
)
}
return (
<Drawer {...props}>
<DrawerContent>{children}</DrawerContent>
</Drawer>
)
}
最佳实践
- 使用 TypeScript:利用完整的类型安全性以获得更好的开发体验
- 自定义组件:直接在代码库中修改组件
- 组合原语:通过组合简单组件构建复杂 UI
- 遵循可访问性:组件基于可访问的 Radix UI 原语构建
- 使用表单验证:集成 React Hook Form + Zod 以实现健壮的表单
- 深色模式:实现主题切换以改善用户体验
- 响应式设计:使用 Tailwind 响应式工具
- 性能:对大型组件集使用代码拆分和懒加载
- 一致间距:一致使用 Tailwind 间距比例
- 图标库:使用 lucide-react 以获取一致的图标
框架特定设置
Next.js
- 支持 App Router 和 Pages Router
- 服务器组件兼容性
- 服务器操作集成
Vite
- 通过 HMR 快速开发
- 轻松设置 TypeScript
Remix
- 基于路由的架构
- 渐进增强
Astro
- 静态站点生成
- 岛屿架构
Laravel(Inertia.js)
- 与 Laravel 的后端集成
- 使用 Inertia 的 React 前端
常见模式
加载状态
import { Skeleton } from "@/components/ui/skeleton"
export function UserCardSkeleton() {
return (
<div className="flex items-center space-x-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
)
}
export function UserCard({ user }: { user?: User }) {
if (!user) return <UserCardSkeleton />
return (
<div className="flex items-center space-x-4">
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarFallback>{user.initials}</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium">{user.name}</p>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
</div>
)
}
错误处理
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { AlertCircle } from "lucide-react"
export function ErrorAlert({ error }: { error: Error }) {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>错误</AlertTitle>
<AlertDescription>{error.message}</AlertDescription>
</Alert>
)
}
确认对话框
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
export function DeleteConfirmation({ onConfirm }: { onConfirm: () => void }) {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">删除</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>您完全确定吗?</AlertDialogTitle>
<AlertDialogDescription>
此操作无法撤销。这将永久删除您的
帐户并从我们的服务器中移除您的数据。
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>继续</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
故障排除
常见问题
-
“模块未找到”错误
- 检查
tsconfig.json中的路径别名 - 验证
components.json别名与项目结构匹配 - 确保组件安装在正确的目录中
- 检查
-
样式未应用
- 验证 Tailwind CSS 是否已正确配置
- 检查
globals.css是否导入了 CSS 变量 - 确保
tailwindcss-animate插件已安装
-
深色模式不工作
- 检查 ThemeProvider 是否包装了您的应用
- 验证
<html>标签上的suppressHydrationWarning - 确保深色模式类在 CSS 中定义
-
表单验证问题
- 安装所需包:
react-hook-form、@hookform/resolvers、zod - 检查模式是否与表单字段匹配
- 验证解析器是否正确配置
- 安装所需包:
-
TypeScript 错误
- 更新
@types/react和@types/react-dom - 检查组件属性类型
- 确保 TypeScript 版本兼容(>= 4.5)
- 更新
资源
- 文档:https://ui.shadcn.com
- GitHub:https://github.com/shadcn-ui/ui
- 组件注册表:https://ui.shadcn.com/docs/components
- 示例:https://ui.shadcn.com/examples
- Figma 设计套件:https://ui.shadcn.com/figma
- v0(AI UI 生成器):https://v0.dev
实施清单
实施 shadcn/ui 时:
- [ ] 运行
npx shadcn@latest init以设置项目 - [ ] 配置 Tailwind CSS 和路径别名
- [ ] 使用 ThemeProvider 设置深色模式
- [ ] 通过 CLI 安装所需组件
- [ ] 创建实用函数(如 cn 助手)
- [ ] 设置表单处理(React Hook Form + Zod)
- [ ] 配置图标(lucide-react)
- [ ] 实现主题切换组件
- [ ] 在浅色和深色模式下测试组件
- [ ] 如有需要,自定义颜色调色板
- [ ] 使用骨架屏添加加载状态
- [ ] 实施错误处理模式
- [ ] 测试可访问性功能
- [ ] 优化捆绑包大小(树摇)
- [ ] 添加响应式设计工具