ByteNoteByteNote

字节笔记本

2026年5月3日

shadcn/ui 完整使用指南 - Next.js 集成

API中转
¥120

shadcn/ui 是一个基于 Radix UI 和 Tailwind CSS 的组件集合。与传统 npm 包不同,它将组件代码直接复制到项目中,提供完全的可定制性。

快速开始

1. 创建 Next.js 项目

bash
npx create-next-app@latest my-app
cd my-app

2. 初始化 shadcn/ui

bash
npx shadcn@latest init

初始化选项

选项说明推荐
样式Default / New YorkDefault(简洁)
基础颜色Slate / Gray / Zinc / Neutral / StoneSlate(优雅)
CSS 变量是否使用 CSS 变量是(便于主题定制)

安装组件

方法一:一次性安装所有组件(推荐)

bash
npx shadcn@latest add --all
npx shadcn@latest add -a        # 简写形式
npx shadcn@latest add --all -y  # 跳过确认提示

方法二:按需安装

bash
npx shadcn@latest add button                    # 安装单个组件
npx shadcn@latest add button card input label   # 安装多个组件

方法三:使用 Bun

bash
bunx shadcn@latest add --all

项目结构

text
my-app/
├── app/
│   ├── layout.tsx          # 根布局
│   └── page.tsx            # 首页
├── components/
│   └── ui/                 # shadcn 组件存放位置
│       ├── button.tsx
│       ├── card.tsx
│       ├── input.tsx
│       └── ...
├── lib/
│   └── utils.ts            # 工具函数(cn 函数)
├── styles/
│   └── globals.css         # 全局样式(主题变量)
├── components.json         # shadcn 配置
├── tailwind.config.ts      # Tailwind 配置
└── tsconfig.json

使用示例

基础用法

typescript
import { Button } from "@/components/ui/button"
import {
  Card, CardContent, CardDescription, CardHeader, CardTitle
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function Home() {
  return (
    <div className="container mx-auto p-8">
      <Card className="max-w-md mx-auto">
        <CardHeader>
          <CardTitle>登录表单</CardTitle>
          <CardDescription>使用 shadcn/ui 组件</CardDescription>
        </CardHeader>
        <CardContent>
          <form className="space-y-4">
            <div className="space-y-2">
              <Label htmlFor="email">邮箱</Label>
              <Input id="email" type="email" placeholder="输入邮箱" />
            </div>
            <div className="space-y-2">
              <Label htmlFor="password">密码</Label>
              <Input id="password" type="password" placeholder="输入密码" />
            </div>
            <Button className="w-full">登录</Button>
          </form>
        </CardContent>
      </Card>
    </div>
  )
}

客户端组件示例

typescript
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useToast } from "@/hooks/use-toast"

export function DemoForm() {
  const [email, setEmail] = useState("")
  const { toast } = useToast()

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    toast({ title: "提交成功", description: `邮箱:${email}` })
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div className="space-y-2">
        <Label htmlFor="email">邮箱</Label>
        <Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
      </div>
      <Button type="submit">提交</Button>
    </form>
  )
}

常用 CLI 命令

bash
npx shadcn@latest add                        # 查看可用组件
npx shadcn@latest view button                # 查看某个组件的预览
npx shadcn@latest view button card dialog    # 查看多个组件
npx shadcn@latest update                     # 更新组件
npx shadcn@latest add button --overwrite     # 覆盖已存在的文件
npx shadcn@latest list                       # 查看已安装组件

主题定制

修改主题颜色

通过 styles/globals.css 中的 CSS 变量自定义主题颜色,支持亮色和暗色模式。

切换深色模式

typescript
"use client"

import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"

export function ThemeToggle() {
  const { theme, setTheme } = useTheme()
  return (
    <Button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
      切换主题
    </Button>
  )
}

常用组件示例

Button

typescript
import { Button } from "@/components/ui/button"

<Button>默认按钮</Button>
<Button variant="destructive">危险按钮</Button>
<Button variant="outline">轮廓按钮</Button>
<Button variant="secondary">次要按钮</Button>
<Button variant="ghost">幽灵按钮</Button>
<Button variant="link">链接按钮</Button>
<Button size="sm">小按钮</Button>
<Button size="lg">大按钮</Button>

Card

typescript
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"

<Card>
  <CardHeader>
    <CardTitle>卡片标题</CardTitle>
    <CardDescription>卡片描述</CardDescription>
  </CardHeader>
  <CardContent><p>卡片内容</p></CardContent>
  <CardFooter><Button>操作</Button></CardFooter>
</Card>

Dialog

typescript
import {
  Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger,
} from "@/components/ui/dialog"

<Dialog>
  <DialogTrigger asChild><Button>打开对话框</Button></DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>对话框标题</DialogTitle>
      <DialogDescription>对话框描述</DialogDescription>
    </DialogHeader>
    <div>对话框内容</div>
  </DialogContent>
</Dialog>

Form(react-hook-form + zod)

typescript
"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"

const formSchema = z.object({
  username: z.string().min(2, { message: "用户名至少2个字符" }),
})

export function ProfileForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { username: "" },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>用户名</FormLabel>
              <FormControl>
                <Input placeholder="输入用户名" {...field} />
              </FormControl>
              <FormDescription>这是你的公开显示名称</FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">提交</Button>
      </form>
    </Form>
  )
}

配置文件

components.json

json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "default",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "slate",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  }
}

lib/utils.ts

typescript
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

常见问题

Q1: 如何修改组件样式?

直接编辑 components/ui 目录下的组件文件,或使用 className prop 覆盖样式。

typescript
<Button className="bg-blue-500 hover:bg-blue-600">自定义样式</Button>

Q2: 如何添加图标?

推荐使用 lucide-react

bash
npm install lucide-react
typescript
import { Mail } from "lucide-react"

<Button>
  <Mail className="mr-2 h-4 w-4" />
  发送邮件
</Button>

Q3: 如何集成表单验证?

使用 react-hook-form + zod

bash
npm install react-hook-form @hookform/resolvers zod

核心优势

  1. 完全控制: 组件代码在你的项目中,可自由修改
  2. 类型安全: 完整的 TypeScript 支持
  3. 易于定制: 基于 Tailwind CSS 和 CSS 变量
  4. 现代化: 支持 Next.js 13+ App Router
  5. 可访问性: 基于 Radix UI,符合 WAI-ARIA 标准

相关资源

分享: