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

该命令会引导你完成配置过程,包括:

  1. 选择组件的默认路径(默认:src/components/ui
  2. 选择样式文件的路径(默认:src/lib/utils.ts
  3. 选择是否使用 CSS 变量(推荐:是)
  4. 选择是否使用 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 的 memouseMemouseCallback 来减少不必要的重新渲染:

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-slotclass-variance-authority
  • 验证 package.json 中的依赖项版本

参考资源

总结

shadcn/ui 是一个独特的 UI 组件库,通过让开发者复制组件代码的方式,给予了完全的控制权和定制能力。它基于 React 和 Tailwind CSS,提供了现代化的设计系统和丰富的组件,非常适合构建需要高度定制的现代化 Web 应用。

使用 shadcn/ui,你可以:

  • 快速集成高质量的 UI 组件
  • 根据需要自由定制组件
  • 保持设计的一致性
  • 构建响应式、无障碍的应用

shadcn/ui 的设计理念——"组件即代码"——代表了 UI 组件库的一种新趋势,它给予开发者更多的自由和控制权,同时提供了现代化的设计系统作为起点。这种方式非常适合那些希望在保持设计一致性的同时,又需要高度定制 UI 的项目。

« 上一篇 HTMX 中文教程 下一篇 » Ant Design 教程 - 企业级 UI 设计语言和 React 组件库