🚀 快速安装
复制以下命令并运行,立即安装此 Skill:
npx @anthropic-ai/skills install giuseppe-trisciuoglio/developer-kit/shadcn-ui
💡 提示:需要 Node.js 和 NPM
shadcn/ui 组件模式
概述
使用 shadcn/ui、Radix UI 和 Tailwind CSS 构建可访问、可自定义的 UI 组件的专家指南。本技能提供了实现生产就绪、完全支持可访问性的组件的全面模式。
目录
何时使用
- 使用 shadcn/ui 设置新项目
- 安装或配置单个组件
- 使用 React Hook Form 和 Zod 验证构建表单
- 创建可访问的 UI 组件(按钮、对话框、下拉菜单、滑出面板)
- 使用 Tailwind CSS 自定义组件样式
- 使用 shadcn/ui 实现设计系统
- 使用 TypeScript 构建 Next.js 应用程序
- 创建复杂布局和数据展示
操作指南
- 初始化项目:运行
npx shadcn@latest init配置 shadcn/ui - 安装组件:使用
npx shadcn@latest add <组件名>添加组件 - 配置主题:在 globals.css 中自定义 CSS 变量以实现主题
- 导入组件:从
@/components/ui/目录使用组件 - 按需自定义:直接在项目中修改组件代码
- 添加表单验证:将 React Hook Form 与 Zod 模式集成
- 测试可访问性:验证 ARIA 属性和键盘导航
示例
带验证的完整表单
"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, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
const formSchema = z.object({
email: z.string().email("无效的邮箱地址"),
password: z.string().min(8, "密码至少需要8个字符"),
})
export function LoginForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { email: "", password: "" },
})
return (
<Form {...form}>
<form onSubmit={form.handleSubmit((data) => console.log(data))} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormControl>
<Input type="email" placeholder="请输入邮箱" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>密码</FormLabel>
<FormControl>
<Input type="password" placeholder="请输入密码" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">登录</Button>
</form>
</Form>
)
}
约束与警告
- 不是 NPM 包:组件被复制到您的项目中;您拥有代码所有权
- 注册表安全:通过
npx shadcn@latest add安装的组件是从远程注册表(例如ui.shadcn.com)获取的;安装前务必验证注册表源是否可信,并在生产环境中使用前审查生成的组件代码 - 自定义注册表验证:在
components.json中配置自定义注册表时,仅使用受信任的私有注册表 URL;切勿指向不受信任的第三方注册表端点,因为它们可能注入恶意代码 - 客户端组件:大多数组件需要 “use client” 指令
- Radix 依赖:确保所有 @radix-ui 包已安装
- 需要 Tailwind:组件依赖于 Tailwind CSS 工具类
- TypeScript:为 TypeScript 项目设计;类型定义已包含
- 路径别名:在 tsconfig.json 中配置 @ 别名用于导入
- 暗黑模式:使用 CSS 变量或类策略设置暗黑模式
快速开始
对于新项目,使用自动化设置:
# 使用 shadcn/ui 创建 Next.js 项目
npx create-next-app@latest my-app --typescript --tailwind --eslint --app
cd my-app
npx shadcn@latest init
# 安装核心组件
npx shadcn@latest add button input form card dialog select
对于现有项目:
# 安装依赖
npm install tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react
# 初始化 shadcn/ui
npx shadcn@latest init
什么是 shadcn/ui?
shadcn/ui 不是 传统的组件库或 npm 包。相反:
- 它是一个可重用组件的集合,您可以将其复制到项目中
- 组件归您所有,可自定义 – 您拥有代码所有权
- 基于 Radix UI 原语构建,确保可访问性
- 使用 Tailwind CSS 工具类进行样式设计
- 包含 CLI 工具,方便安装组件
安装与设置
初始设置
# 在项目中初始化 shadcn/ui
npx shadcn@latest init
在设置过程中,您将配置:
- TypeScript 或 JavaScript
- 样式(Default, New York 等)
- 基础颜色主题
- CSS 变量或 Tailwind CSS 类
- 组件安装路径
安装单个组件
# 安装单个组件
npx shadcn@latest add button
# 安装多个组件
npx shadcn@latest add button input form
# 安装所有组件
npx shadcn@latest add --all
手动安装
如果您更喜欢手动设置:
# 安装特定组件的依赖
npm install @radix-ui/react-slot
# 从 ui.shadcn.com 复制组件代码
# 放置在 src/components/ui/
项目配置
所需依赖
{
"dependencies": {
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.294.0",
"tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7"
}
}
TSConfig 配置
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Tailwind 配置
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
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))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
CSS 变量 (globals.css)
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-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%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
核心组件
按钮组件
安装:
npx shadcn@latest add button
基本用法:
import { Button } from "@/components/ui/button";
export function ButtonDemo() {
return <Button>点击我</Button>;
}
按钮变体:
import { Button } from "@/components/ui/button";
export function ButtonVariants() {
return (
<div className="flex gap-4">
<Button variant="default">默认</Button>
<Button variant="destructive">危险</Button>
<Button variant="outline">轮廓</Button>
<Button variant="secondary">次要</Button>
<Button variant="ghost">幽灵</Button>
<Button variant="link">链接</Button>
</div>
);
}
按钮尺寸:
<div className="flex gap-4 items-center">
<Button size="default">默认</Button>
<Button size="sm">小</Button>
<Button size="lg">大</Button>
<Button size="icon">
<Icon className="h-4 w-4" />
</Button>
</div>
带加载状态:
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
export function ButtonLoading() {
return (
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
请稍候
</Button>
);
}
输入与表单字段
输入组件
安装:
npx shadcn@latest add input
基础输入:
import { Input } from "@/components/ui/input";
export function InputDemo() {
return <Input type="email" placeholder="邮箱" />;
}
带标签的输入:
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function InputWithLabel() {
return (
<div className="grid w-full max-w-sm items-center gap-1.5">
<Label htmlFor="email">邮箱</Label>
<Input type="email" id="email" placeholder="邮箱" />
</div>
);
}
带按钮的输入:
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export function InputWithButton() {
return (
<div className="flex w-full max-w-sm items-center gap-2">
<Input type="email" placeholder="邮箱" />
<Button type="submit" variant="outline">订阅</Button>
</div>
);
}
带验证的表单
安装:
npx shadcn@latest add form
这将安装 React Hook Form、Zod 和表单组件。
完整表单示例:
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as 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 { toast } from "@/components/ui/use-toast"
const formSchema = z.object({
username: z.string().min(2, {
message: "用户名至少需要2个字符",
}),
email: z.string().email({
message: "请输入有效的邮箱地址",
}),
})
export function ProfileForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
toast({
title: "您提交了以下值:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(values, null, 2)}</code>
</pre>
),
})
}
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>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormControl>
<Input type="email" placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">提交</Button>
</form>
</Form>
)
}
卡片组件
安装:
npx shadcn@latest add card
基础卡片:
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
export function CardDemo() {
return (
<Card>
<CardHeader>
<CardTitle>卡片标题</CardTitle>
<CardDescription>卡片描述</CardDescription>
</CardHeader>
<CardContent>
<p>卡片内容</p>
</CardContent>
<CardFooter>
<p>卡片页脚</p>
</CardFooter>
</Card>
)
}
带表单的卡片:
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function CardWithForm() {
return (
<Card className="w-[350px]">
<CardHeader>
<CardTitle>创建项目</CardTitle>
<CardDescription>一键部署您的新项目。</CardDescription>
</CardHeader>
<CardContent>
<form>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">项目名称</Label>
<Input id="name" placeholder="您的项目名称" />
</div>
</div>
</form>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">取消</Button>
<Button>部署</Button>
</CardFooter>
</Card>
)
}
对话框(模态框)组件
安装:
npx shadcn@latest add dialog
基础对话框:
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
export function DialogDemo() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">打开对话框</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>编辑个人资料</DialogTitle>
<DialogDescription>
在此处更改您的个人资料。完成后点击保存。
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
姓名
</Label>
<Input id="name" value="张三" className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit">保存更改</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
滑出面板组件
安装:
npx shadcn@latest add sheet
基础滑出面板:
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
export function SheetDemo() {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">打开滑出面板</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>编辑个人资料</SheetTitle>
<SheetDescription>
在此处更改您的个人资料。完成后点击保存。
</SheetDescription>
</SheetHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
姓名
</Label>
<Input id="name" value="张三" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="username" className="text-right">
用户名
</Label>
<Input id="username" value="@zhangsan" className="col-span-3" />
</div>
</div>
</SheetContent>
</Sheet>
)
}
带位置选项的滑出面板:
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">打开右侧面板</Button>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>设置</SheetTitle>
<SheetDescription>
在此处配置您的应用程序设置。
</SheetDescription>
</SheetHeader>
{/* 设置内容 */}
</SheetContent>
</Sheet>
菜单栏与导航
菜单栏组件
安装:
npx shadcn@latest add menubar
基础菜单栏:
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from "@/components/ui/menubar"
export function MenubarDemo() {
return (
<Menubar>
<MenubarMenu>
<MenubarTrigger>文件</MenubarTrigger>
<MenubarContent>
<MenubarItem>
新建标签页 <MenubarShortcut>⌘T</MenubarShortcut>
</MenubarItem>
<MenubarItem>
新建窗口 <MenubarShortcut>⌘N</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>分享</MenubarItem>
<MenubarSeparator />
<MenubarItem>打印</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>编辑</MenubarTrigger>
<MenubarContent>
<MenubarItem>
撤销 <MenubarShortcut>⌘Z</MenubarShortcut>
</MenubarItem>
<MenubarItem>
重做 <MenubarShortcut>⌘Y</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>查找</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem>搜索网页</MenubarItem>
<MenubarItem>查找...</MenubarItem>
<MenubarItem>查找下一个</MenubarItem>
<MenubarItem>查找上一个</MenubarItem>
</MenubarSubContent>
</MenubarSub>
</MenubarContent>
</MenubarMenu>
</Menubar>
)
}
选择(下拉框)组件
安装:
npx shadcn@latest add select
基础选择:
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
export function SelectDemo() {
return (
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="选择水果" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">苹果</SelectItem>
<SelectItem value="banana">香蕉</SelectItem>
<SelectItem value="orange">橘子</SelectItem>
</SelectContent>
</Select>
)
}
表单中的选择:
<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="user">用户</SelectItem>
<SelectItem value="guest">访客</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
Toast 通知
安装:
npx shadcn@latest add toast
在根布局中设置 Toast 提供者:
import { Toaster } from "@/components/ui/toaster"
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Toaster />
</body>
</html>
)
}
使用 Toast:
import { useToast } from "@/components/ui/use-toast"
import { Button } from "@/components/ui/button"
export function ToastDemo() {
const { toast } = useToast()
return (
<Button
onClick={() => {
toast({
title: "已安排:跟进会议",
description: "2023年2月10日,星期五,下午5:57",
})
}}
>
显示 Toast
</Button>
)
}
Toast 变体:
// 成功
toast({
title: "成功",
description: "您的更改已保存。",
})
// 错误
toast({
variant: "destructive",
title: "错误",
description: "出错了。",
})
// 带操作
toast({
title: "哎呀!出错了。",
description: "您的请求出现问题。",
action: <ToastAction altText="重试">重试</ToastAction>,
})
表格组件
安装:
npx shadcn@latest add table
基础表格:
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const invoices = [
{ invoice: "INV001", status: "已支付", method: "信用卡", amount: "¥250.00" },
{ invoice: "INV002", status: "处理中", method: "支付宝", amount: "¥150.00" },
]
export function TableDemo() {
return (
<Table>
<TableCaption>您最近的发票列表。</TableCaption>
<TableHeader>
<TableRow>
<TableHead>发票号</TableHead>
<TableHead>状态</TableHead>
<TableHead>支付方式</TableHead>
<TableHead className="text-right">金额</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.invoice}>
<TableCell className="font-medium">{invoice.invoice}</TableCell>
<TableCell>{invoice.status}</TableCell>
<TableCell>{invoice.method}</TableCell>
<TableCell className="text-right">{invoice.amount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
图表组件
安装:
npx shadcn@latest add chart
shadcn/ui 中的图表组件基于 Recharts 构建 – 提供对所有 Recharts 功能的直接访问,并具有一致的主题和样式。
ChartContainer 和 ChartConfig
ChartContainer 包装您的 Recharts 组件,并接受用于主题的 config 属性:
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
import { ChartContainer, ChartTooltipContent } from "@/components/ui/chart"
const chartConfig = {
desktop: {
label: "桌面端",
color: "var(--chart-1)",
},
mobile: {
label: "移动端",
color: "var(--chart-2)",
},
} satisfies import("@/components/ui/chart").ChartConfig
const chartData = [
{ month: "一月", desktop: 186, mobile: 80 },
{ month: "二月", desktop: 305, mobile: 200 },
{ month: "三月", desktop: 237, mobile: 120 },
]
export function BarChartDemo() {
return (
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
<BarChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<Bar
dataKey="desktop"
fill="var(--color-desktop)"
radius={4}
/>
<Bar
dataKey="mobile"
fill="var(--color-mobile)"
radius={4}
/>
<ChartTooltip content={<ChartTooltipContent />} />
</BarChart>
</ChartContainer>
)
}
带自定义颜色的 ChartConfig
您可以直接在配置中定义自定义颜色:
const chartConfig = {
visitors: {
label: "访问量",
color: "#2563eb", // 自定义十六进制颜色
theme: {
light: "#2563eb",
dark: "#60a5fa",
},
},
sales: {
label: "销售额",
color: "var(--chart-1)", // CSS 变量
theme: {
light: "oklch(0.646 0.222 41.116)",
dark: "oklch(0.696 0.182 281.41)",
},
},
} satisfies import("@/components/ui/chart").ChartConfig
图表的 CSS 变量
将图表颜色变量添加到您的 globals.css 中:
@layer base {
:root {
/* 图表颜色 */
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.546 0.198 38.228);
--chart-4: oklch(0.596 0.151 343.253);
--chart-5: oklch(0.546 0.158 49.157);
}
.dark {
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.698 0.141 24.311);
--chart-4: oklch(0.676 0.172 171.196);
--chart-5: oklch(0.578 0.192 302.85);
}
}
折线图示例
import { Line, LineChart, CartesianGrid, XAxis, YAxis } from "recharts"
import { ChartContainer, ChartTooltipContent } from "@/components/ui/chart"
const chartConfig = {
price: {
label: "价格",
color: "var(--chart-1)",
},
} satisfies import("@/components/ui/chart").ChartConfig
const chartData = [
{ month: "一月", price: 186 },
{ month: "二月", price: 305 },
{ month: "三月", price: 237 },
{ month: "四月", price: 203 },
{ month: "五月", price: 276 },
]
export function LineChartDemo() {
return (
<ChartContainer config={chartConfig} className="min-h-[200px]">
<LineChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" tickLine={false} axisLine={false} />
<YAxis tickLine={false} axisLine={false} tickFormatter={(value) => `¥${value}`} />
<Line
dataKey="price"
stroke="var(--color-price)"
strokeWidth={2}
dot={false}
/>
<ChartTooltip content={<ChartTooltipContent />} />
</LineChart>
</ChartContainer>
)
}
面积图示例
import { Area, AreaChart, XAxis, YAxis } from "recharts"
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltipContent } from "@/components/ui/chart"
const chartConfig = {
desktop: { label: "桌面端", color: "var(--chart-1)" },
mobile: { label: "移动端", color: "var(--chart-2)" },
} satisfies import("@/components/ui/chart").ChartConfig
export function AreaChartDemo() {
return (
<ChartContainer config={chartConfig} className="min-h-[200px]">
<AreaChart data={chartData}>
<XAxis dataKey="month" tickLine={false} axisLine={false} />
<YAxis tickLine={false} axisLine={false} />
<Area
dataKey="desktop"
fill="var(--color-desktop)"
stroke="var(--color-desktop)"
fillOpacity={0.3}
/>
<Area
dataKey="mobile"
fill="var(--color-mobile)"
stroke="var(--color-mobile)"
fillOpacity={0.3}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
</AreaChart>
</ChartContainer>
)
}
饼图示例
import { Pie, PieChart } from "recharts"
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltipContent } from "@/components/ui/chart"
const chartConfig = {
chrome: { label: "Chrome", color: "var(--chart-1)" },
safari: { label: "Safari", color: "var(--chart-2)" },
firefox: { label: "Firefox", color: "var(--chart-3)" },
} satisfies import("@/components/ui/chart").ChartConfig
const pieData = [
{ browser: "Chrome", visitors: 275, fill: "var(--color-chrome)" },
{ browser: "Safari", visitors: 200, fill: "var(--color-safari)" },
{ browser: "Firefox", visitors: 187, fill: "var(--color-firefox)" },
]
export function PieChartDemo() {
return (
<ChartContainer config={chartConfig} className="min-h-[200px]">
<PieChart>
<Pie
data={pieData}
dataKey="visitors"
nameKey="browser"
cx="50%"
cy="50%"
outerRadius={80}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
</PieChart>
</ChartContainer>
)
}
ChartTooltipContent 属性
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
labelKey |
字符串 | “label” | 工具提示标签的键 |
nameKey |
字符串 | “name” | 工具提示名称的键 |
indicator |
“dot” | “line” | “dashed” | “dot” | 指示器样式 |
hideLabel |
布尔值 | false | 隐藏标签 |
hideIndicator |
布尔值 | false | 隐藏指示器 |
可访问性
启用键盘导航和屏幕阅读器支持:
<BarChart accessibilityLayer data={chartData}>...</BarChart>
这将添加:
- 键盘箭头键导航
- 图表元素的 ARIA 标签
- 数据值的屏幕阅读器朗读
自定义
使用 CSS 变量进行主题设置
shadcn/ui 使用 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%;
/* ... 其他暗黑模式变量 */
}
}
自定义组件
由于您拥有代码所有权,可以直接自定义:
// components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent",
// 添加自定义变体
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",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
Next.js 集成
App Router 设置
对于使用 App Router 的 Next.js 13+,确保组件使用 "use client" 指令:
// src/components/ui/button.tsx
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
// ... 组件的其余部分
布局集成
将 Toaster 添加到根布局:
// app/layout.tsx
import { Toaster } from "@/components/ui/toaster"
import "./globals.css"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<body className="min-h-screen bg-background font-sans antialiased">
{children}
<Toaster />
</body>
</html>
)
}
服务端组件
在服务端组件中使用 shadcn/ui 组件时,将它们包装在客户端组件中:
// app/dashboard/page.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ButtonClient } from "@/components/ui/button-client"
export default function DashboardPage() {
return (
<div className="container mx-auto p-6">
<Card>
<CardHeader>
<CardTitle>仪表盘</CardTitle>
</CardHeader>
<CardContent>
<ButtonClient>交互式按钮</ButtonClient>
</CardContent>
</Card>
</div>
)
}
// src/components/ui/button-client.tsx
"use client"
import { Button } from "./button"
export function ButtonClient(props: React.ComponentProps<typeof Button>) {
return <Button {...props} />
}
带表单的路由处理器
为表单提交创建 API 路由:
// app/api/contact/route.ts
import { NextRequest, NextResponse } from "next/server"
import { z } from "zod"
const contactSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10),
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validated = contactSchema.parse(body)
// 处理表单数据
console.log("表单提交:", validated)
return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ errors: error.errors },
{ status: 400 }
)
}
return NextResponse.json(
{ error: "内部服务器错误" },
{ status: 500 }
)
}
}
使用服务器操作的表单
使用 Next.js 14+ 服务器操作:
// app/contact/page.tsx
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { toast } from "@/components/ui/use-toast"
const formSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10),
})
async function onSubmit(values: z.infer<typeof formSchema>) {
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
})
if (!response.ok) throw new Error("提交失败")
toast({
title: "成功!",
description: "您的消息已发送。",
})
} catch (error) {
toast({
variant: "destructive",
title: "错误",
description: "发送消息失败。请重试。",
})
}
}
export default function ContactPage() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
})
return (
<div className="container mx-auto max-w-2xl py-8">
<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="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>消息</FormLabel>
<FormControl>
<Textarea
placeholder="请输入您的消息..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
发送消息
</Button>
</form>
</Form>
</div>
)
}
使用 shadcn/ui 的元数据
在元数据中使用 shadcn/ui 组件:
// app/layout.tsx
import { Metadata } from "next"
export const metadata: Metadata = {
title: {
default: "我的应用",
template: "%s | 我的应用",
},
description: "使用 shadcn/ui 和 Next.js 构建",
}
// app/about/page.tsx
import { Metadata } from "next"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
export const metadata: Metadata = {
title: "关于我们",
description: "了解更多关于我们的公司信息",
}
export default function AboutPage() {
return (
<div className="container mx-auto py-8">
<Card>
<CardHeader>
<CardTitle>关于我们的公司</CardTitle>
</CardHeader>
<CardContent>
<p>我们使用现代网络技术构建令人惊叹的产品。</p>
</CardContent>
</Card>
</div>
)
}
字体优化
使用 next/font 优化字体:
// app/layout.tsx
import { Inter } from "next/font/google"
import { Toaster } from "@/components/ui/toaster"
import { cn } from "@/lib/utils"
import "./globals.css"
const inter = Inter({ subsets: ["latin"] })
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<body className={cn(
"min-h-screen bg-background font-sans antialiased",
inter.className
)}>
{children}
<Toaster />
</body>
</html>
)
}
高级模式
多字段表单
const formSchema = z.object({
username: z.string().min(2).max(50),
email: z.string().email(),
bio: z.string().max(160).min(4),
role: z.enum(["admin", "user", "guest"]),
notifications: z.boolean().default(false),
})
export function AdvancedForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
bio: "",
role: "user",
notifications: false,
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* 用户名等字段 */}
{/* ... */}
</form>
</Form>
)
}
最佳实践
- 可访问性:组件使用 Radix UI 原语确保 ARIA 合规性
- 自定义:直接在您的代码库中修改组件
- 类型安全:使用 TypeScript 实现类型安全的属性和状态
- 验证:使用 Zod 模式进行表单验证
- 样式:利用 Tailwind 工具类和 CSS 变量
- 一致性:在整个应用中保持相同的组件模式
- 测试:组件可使用 React Testing Library 进行测试
- 性能:组件已优化并可进行 tree-shaking
常用组件组合
登录表单
<Card className="w-[350px]">
<CardHeader>
<CardTitle>登录</CardTitle>
<CardDescription>请输入您的凭据继续</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{/* 表单字段 */}
<Button type="submit" className="w-full">登录</Button>
</form>
</Form>
</CardContent>
</Card>
参考链接
- 官方文档:https://ui.shadcn.com
- Radix UI:https://www.radix-ui.com
- React Hook Form:https://react-hook-form.com
- Zod:https://zod.dev
- Tailwind CSS:https://tailwindcss.com
- 示例:https://ui.shadcn.com/examples
📄 原始文档
完整文档(英文):
https://skills.sh/giuseppe-trisciuoglio/developer-kit/shadcn-ui
💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。

评论(0)