shadcn/ui 教程
项目概述
shadcn/ui 是一个可定制的 UI 组件库,与传统的组件库不同,它采用了一种独特的分发模式:开发者只需复制组件代码即可集成到项目中。这种方式给予开发者完全的控制权,可以根据需要自由修改和定制组件。shadcn/ui 基于 React 和 Tailwind CSS,提供了现代化的设计系统和丰富的组件。
主要特点
- 可定制性:开发者拥有组件代码的完全控制权,可以自由修改和定制
- 现代化设计:采用现代、简洁的设计风格
- 响应式:组件完全响应式,适配各种屏幕尺寸
- 无障碍支持:符合 WCAG 标准,支持屏幕阅读器
- TypeScript 支持:完整的类型定义
- 基于 Tailwind CSS:利用 Tailwind CSS 的工具类进行样式设计
- 组件丰富:提供了常用的 UI 组件
适用场景
- 构建现代化的 Web 应用
- 需要高度定制 UI 的项目
- 希望保持设计一致性的团队
- 快速原型开发
- 企业级应用
安装与设置
前提条件
在使用 shadcn/ui 之前,需要确保项目满足以下条件:
- React 18+
- Tailwind CSS v3+
- TypeScript(推荐)
初始化项目
如果还没有项目,可以使用 Vite 创建一个新的 React + TypeScript 项目:
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install安装 Tailwind CSS
按照 Tailwind CSS 官方文档的指导安装和配置 Tailwind CSS:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p配置 tailwind.config.js:
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}在 src/index.css 中添加 Tailwind 指令:
@tailwind base;
@tailwind components;
@tailwind utilities;安装 shadcn/ui
使用 npx shadcn-ui@latest init 命令初始化 shadcn/ui:
npx shadcn-ui@latest init该命令会引导你完成配置过程,包括:
- 选择组件的默认路径(默认:
src/components/ui) - 选择样式文件的路径(默认:
src/lib/utils.ts) - 选择是否使用 CSS 变量(推荐:是)
- 选择是否使用 React Server Components(根据项目需求选择)
安装组件
使用 npx shadcn-ui@latest add 命令添加组件:
# 添加按钮组件
npx shadcn-ui@latest add button
# 添加卡片组件
npx shadcn-ui@latest add card
# 添加输入框组件
npx shadcn-ui@latest add input
# 批量添加多个组件
npx shadcn-ui@latest add dropdown-menu dialog alert核心概念
1. 组件结构
shadcn/ui 的组件通常由以下几个部分组成:
- 主组件文件:包含组件的主要逻辑和结构
- 样式文件:包含组件的样式定义(使用 Tailwind CSS)
- 类型定义:包含组件的 TypeScript 类型定义
例如,按钮组件的结构:
src/
components/
ui/
button.tsx # 主组件文件
button-group.tsx # 按钮组组件2. 样式系统
shadcn/ui 使用 Tailwind CSS 作为样式系统,并通过 CSS 变量实现主题定制。
主题配置
主题配置位于 tailwind.config.js 文件中,通过 extend 选项扩展默认主题:
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
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))",
},
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: [],
}CSS 变量
CSS 变量定义在 src/app/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;
}
}3. 组件使用
基本使用
安装组件后,可以直接在项目中导入和使用:
import { Button } from "@/components/ui/button";
function App() {
return (
<div>
<Button>Click me</Button>
</div>
);
}
export default App;组件定制
由于组件代码已经复制到项目中,你可以根据需要自由修改组件:
// src/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 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
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 hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
// 自定义变体
custom: "bg-gradient-to-r from-blue-500 to-purple-600 text-white hover:from-blue-600 hover:to-purple-700",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
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 };常用组件
1. Button
按钮组件是最常用的 UI 组件之一,shadcn/ui 提供了多种变体和尺寸。
import { Button } from "@/components/ui/button";
function ButtonExample() {
return (
<div className="flex flex-wrap gap-2">
<Button>Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button disabled>Disabled</Button>
</div>
);
}
export default ButtonExample;2. Card
卡片组件用于展示相关信息的集合。
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
function CardExample() {
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description goes here</CardDescription>
</CardHeader>
<CardContent>
<p>Card content goes here. You can add any HTML elements or other components.</p>
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
);
}
export default CardExample;3. Input
输入框组件用于接收用户输入。
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
function InputExample() {
return (
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="Enter your email" />
</div>
);
}
export default InputExample;4. Form
表单组件用于收集用户输入。
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
function FormExample() {
return (
<form className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Enter your name" />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="Enter your email" />
</div>
<div className="space-y-2">
<Label htmlFor="message">Message</Label>
<Textarea id="message" placeholder="Enter your message" />
</div>
<Button type="submit">Submit</Button>
</form>
);
}
export default FormExample;5. Dialog
对话框组件用于显示重要信息或请求用户确认。
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
function DialogExample() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>Dialog description goes here</DialogDescription>
</DialogHeader>
<div className="py-4">
Dialog content goes here
</div>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export default DialogExample;6. Dropdown Menu
下拉菜单组件用于显示一组选项。
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
function DropdownMenuExample() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Open Menu</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Option 1</DropdownMenuItem>
<DropdownMenuItem>Option 2</DropdownMenuItem>
<DropdownMenuItem>Option 3</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
export default DropdownMenuExample;7. Tabs
标签页组件用于在同一页面中切换不同的内容。
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
function TabsExample() {
return (
<Tabs defaultValue="tab1">
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
<TabsTrigger value="tab3">Tab 3</TabsTrigger>
</TabsList>
<TabsContent value="tab1">
Content for Tab 1
</TabsContent>
<TabsContent value="tab2">
Content for Tab 2
</TabsContent>
<TabsContent value="tab3">
Content for Tab 3
</TabsContent>
</Tabs>
);
}
export default TabsExample;8. Avatar
头像组件用于显示用户头像。
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
function AvatarExample() {
return (
<Avatar>
<AvatarImage src="https://example.com/avatar.jpg" alt="User avatar" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>
);
}
export default AvatarExample;高级功能
1. 主题切换
shadcn/ui 内置了深色模式支持,可以通过切换 dark 类来实现主题切换:
import { Button } from "@/components/ui/button";
function ThemeToggle() {
const toggleTheme = () => {
if (document.documentElement.classList.contains("dark")) {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
};
return (
<Button onClick={toggleTheme} variant="outline">
Toggle Theme
</Button>
);
}
export default ThemeToggle;2. 自定义组件
除了使用内置组件外,你还可以基于 shadcn/ui 的设计系统创建自定义组件:
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const cardVariants = cva(
"rounded-lg border bg-card text-card-foreground shadow-sm",
{
variants: {
variant: {
default: "",
elevated: "shadow-md",
outline: "border-2",
},
size: {
default: "p-6",
sm: "p-4",
lg: "p-8",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface CustomCardProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof cardVariants> {
title?: string;
children: React.ReactNode;
}
const CustomCard = React.forwardRef<HTMLDivElement, CustomCardProps>(
({ className, variant, size, title, children, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(cardVariants({ variant, size, className }))}
{...props}
>
{title && <h3 className="text-lg font-medium mb-4">{title}</h3>}
{children}
</div>
);
}
);
CustomCard.displayName = "CustomCard";
export { CustomCard, cardVariants };3. 组件组合
shadcn/ui 的组件设计遵循组合原则,可以轻松组合多个组件来创建更复杂的 UI:
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
function CombinedComponent() {
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>User Profile</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="personal">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="personal">Personal Info</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="personal" className="space-y-4 pt-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input id="name" defaultValue="John Doe" />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" defaultValue="john@example.com" />
</div>
</TabsContent>
<TabsContent value="settings" className="space-y-4 pt-4">
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" placeholder="Enter new password" />
</div>
<div className="flex items-center space-x-2">
<input type="checkbox" id="notifications" />
<Label htmlFor="notifications">Enable notifications</Label>
</div>
</TabsContent>
</Tabs>
</CardContent>
<div className="p-6 flex justify-end space-x-2 border-t">
<Button variant="outline">Cancel</Button>
<Button>Save Changes</Button>
</div>
</Card>
);
}
export default CombinedComponent;性能优化
1. 组件懒加载
对于大型组件,可以使用 React 的懒加载功能来减少初始加载时间:
import { lazy, Suspense } from "react";
import { Button } from "@/components/ui/button";
const HeavyComponent = lazy(() => import("./HeavyComponent"));
function App() {
return (
<div>
<Button>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
</Button>
</div>
);
}
export default App;2. 减少重新渲染
使用 React 的 memo、useMemo 和 useCallback 来减少不必要的重新渲染:
import { memo, useMemo, useCallback } from "react";
import { Button } from "@/components/ui/button";
const ExpensiveCalculation = memo(({ value }) => {
const result = useMemo(() => {
// 昂贵的计算
for (let i = 0; i < 100000000; i++) {
// 计算
}
return value * 2;
}, [value]);
return <div>Result: {result}</div>;
});
function App() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<Button onClick={increment}>
Increment: {count}
</Button>
<ExpensiveCalculation value={count} />
</div>
);
}
export default App;3. 代码分割
使用 React Router 等库进行代码分割,减少初始包大小:
import { createBrowserRouter, RouterProvider } from "react-router-dom";
const Home = lazy(() => import("./routes/Home"));
const About = lazy(() => import("./routes/About"));
const Dashboard = lazy(() => import("./routes/Dashboard"));
const router = createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "/about",
element: <About />,
},
{
path: "/dashboard",
element: <Dashboard />,
},
]);
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<RouterProvider router={router} />
</Suspense>
);
}
export default App;最佳实践
1. 组件组织
- 按功能模块组织组件
- 使用文件夹结构管理相关组件
- 为组件添加清晰的文档和注释
2. 样式管理
- 尽量使用 Tailwind CSS 的工具类
- 避免使用内联样式
- 对于复杂的样式,可以使用
@layer定义自定义工具类
3. 可访问性
- 确保所有组件都有适当的 ARIA 属性
- 测试组件在屏幕阅读器中的表现
- 确保键盘导航正常工作
4. 测试
- 为组件编写单元测试
- 测试组件在不同主题下的表现
- 测试组件的响应式行为
5. 性能
- 优化组件渲染性能
- 减少不必要的重新渲染
- 使用代码分割减少初始加载时间
常见问题与解决方案
1. 组件样式问题
问题:组件样式不符合预期。
解决方案:
- 确保 Tailwind CSS 配置正确
- 检查是否正确导入了全局样式
- 验证组件类名是否正确
2. 组件导入错误
问题:无法导入组件。
解决方案:
- 检查组件路径是否正确
- 确保组件文件存在
- 验证 tsconfig.json 中的路径别名配置
3. 主题切换不生效
问题:深色模式切换不生效。
解决方案:
- 确保全局样式中包含了深色模式的 CSS 变量
- 检查
dark类是否正确添加到html元素 - 验证组件是否使用了 CSS 变量而不是硬编码的颜色
4. 组件定制问题
问题:修改组件后没有生效。
解决方案:
- 确保修改的是正确的组件文件
- 检查是否有缓存问题
- 验证修改是否符合组件的设计模式
5. 依赖项问题
问题:缺少组件依赖项。
解决方案:
- 运行
npm install安装所有依赖 - 检查是否缺少特定的依赖包,如
@radix-ui/react-slot或class-variance-authority - 验证 package.json 中的依赖项版本
参考资源
总结
shadcn/ui 是一个独特的 UI 组件库,通过让开发者复制组件代码的方式,给予了完全的控制权和定制能力。它基于 React 和 Tailwind CSS,提供了现代化的设计系统和丰富的组件,非常适合构建需要高度定制的现代化 Web 应用。
使用 shadcn/ui,你可以:
- 快速集成高质量的 UI 组件
- 根据需要自由定制组件
- 保持设计的一致性
- 构建响应式、无障碍的应用
shadcn/ui 的设计理念——"组件即代码"——代表了 UI 组件库的一种新趋势,它给予开发者更多的自由和控制权,同时提供了现代化的设计系统作为起点。这种方式非常适合那些希望在保持设计一致性的同时,又需要高度定制 UI 的项目。