🚀 快速安装

复制以下命令并运行,立即安装此 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 应用程序
  • 创建复杂布局和数据展示

操作指南

  1. 初始化项目:运行 npx shadcn@latest init 配置 shadcn/ui
  2. 安装组件:使用 npx shadcn@latest add <组件名> 添加组件
  3. 配置主题:在 globals.css 中自定义 CSS 变量以实现主题
  4. 导入组件:从 @/components/ui/ 目录使用组件
  5. 按需自定义:直接在项目中修改组件代码
  6. 添加表单验证:将 React Hook Form 与 Zod 模式集成
  7. 测试可访问性:验证 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>
  )
}

最佳实践

  1. 可访问性:组件使用 Radix UI 原语确保 ARIA 合规性
  2. 自定义:直接在您的代码库中修改组件
  3. 类型安全:使用 TypeScript 实现类型安全的属性和状态
  4. 验证:使用 Zod 模式进行表单验证
  5. 样式:利用 Tailwind 工具类和 CSS 变量
  6. 一致性:在整个应用中保持相同的组件模式
  7. 测试:组件可使用 React Testing Library 进行测试
  8. 性能:组件已优化并可进行 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://skills.sh/giuseppe-trisciuoglio/developer-kit/shadcn-ui

💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。