12k
All articles

如何将 ShadCN 与 Next.js 集成

通过 CLI 逐步将 Shadcn UI 集成到 Next.js 项目,涵盖 Tailwind CSS 配置、next-themes 暗色模式及 React 19 兼容性处理。

OpenReplay Team
OpenReplay Team
如何将 ShadCN 与 Next.js 集成

设置现代 UI 组件库不应该像在没有说明书的情况下组装家具一样困难。如果您正在使用 Next.js 15 构建应用,并希望获得美观、可定制的组件,而不必忍受传统库的黑盒挫败感,本指南将向您展示如何将 Shadcn UI 集成到您的项目中。

本文涵盖了完整的 Shadcn UI Next.js 设置过程,从初始项目创建到组件安装、Tailwind CSS 配置和正确的暗色模式实现。您将学会如何避免常见的 React 19 兼容性问题,并设置一个您真正拥有的生产就绪组件系统。

关键要点

  • Shadcn UI 将组件源代码直接复制到您的项目中,为您提供完全的所有权和定制控制
  • CLI 处理安装和依赖管理,即使考虑到 React 19 兼容性问题,设置也很简单
  • Tailwind CSS 集成无缝,具有内置主题支持和暗色模式实现
  • 组件与框架无关,可与 Next.js 中的 App Router 和 Pages Router 配合使用

为什么为您的 Next.js 项目选择 Shadcn UI

Shadcn UI 采用了与组件库根本不同的方法。它不是安装一个您无法修改的依赖项,而是将组件源代码直接复制到您的项目中。这意味着您可以获得:

  • 完全拥有您的组件 - 随时修改任何内容
  • Tailwind 优先设计,与您现有的样式无缝集成
  • 零运行时开销 - 组件只是您项目中的代码
  • 内置类型安全
  • 默认正确处理可访问性,通过 Radix UI 原语实现

这种方法在 Next.js 15 的 App Router 中特别出色,其中服务器组件和客户端边界需要仔细的组件设计。

先决条件和初始设置

在开始您的 Shadcn UI Next.js 设置之前,请确保您具备:

  • Node.js 18.17 或更高版本
  • 包管理器(npm、pnpm、yarn 或 bun)
  • 对 React 和 Tailwind CSS 的基本了解

创建您的 Next.js 15 项目

首先使用 TypeScript 和 Tailwind CSS 创建一个新的 Next.js 项目:

npx create-next-app@latest my-app --typescript --tailwind --app

当提示时,选择这些选项:

  • ✓ Would you like to use ESLint? Yes
  • ✓ Would you like to use src/ directory? Yes(推荐)
  • ✓ Would you like to customize the default import alias? No

导航到您的项目:

cd my-app

安装和配置 Shadcn UI

Shadcn CLI 处理组件安装的大部分繁重工作。运行初始化命令:

npx shadcn@latest init

您将看到几个配置提示:

  1. Style:选择您喜欢的样式(Default/New York)
  2. Base color:从可用的颜色方案中选择
  3. CSS variables:选择 Yes 以获得更好的主题支持

处理 React 19 对等依赖

如果您在 React 19 中使用 npm,您将遇到对等依赖警告:

npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree

CLI 将提示您选择解决策略:

? How would you like to proceed?
 Use --force
  Use --legacy-peer-deps

选择 --legacy-peer-deps 以获得最安全的方法。pnpm、yarn 或 bun 的用户不会看到这些警告,可以正常进行。

添加您的第一个组件

初始化 Shadcn 后,使用 CLI 添加组件:

npx shadcn@latest add button card

此命令:

  1. 下载组件源代码
  2. 将其放置在 src/components/ui/
  3. 安装任何所需的依赖项(如 Radix UI 原语)
  4. 更新您的组件导入

以下是安装后典型的 Shadcn 按钮组件的样子:

// 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 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2",
  {
    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",
        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",
      },
      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 }

注意组件是完全可定制的 - 您可以修改变体、添加新变体或完全更改样式。

为 Shadcn 配置 Tailwind CSS

Shadcn UI 与 Tailwind CSS v3 和 v4 都兼容。CLI 在初始化期间自动配置您的 tailwind.config.js。以下是添加的内容:

// tailwind.config.js
module.exports = {
  darkMode: ["class"],
  content: [
    './pages/**/*.{ts,tsx}',
    './components/**/*.{ts,tsx}',
    './app/**/*.{ts,tsx}',
    './src/**/*.{ts,tsx}',
  ],
  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)",
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
}

更新您的 globals.css

CLI 还会使用主题的 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: 221.2 83.2% 53.3%;
    --primary-foreground: 210 40% 98%;
    --secondary: 210 40% 96%;
    --secondary-foreground: 222.2 84% 4.9%;
    --muted: 210 40% 96%;
    --muted-foreground: 215.4 16.3% 46.9%;
    --accent: 210 40% 96%;
    --accent-foreground: 222.2 84% 4.9%;
    --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: 221.2 83.2% 53.3%;
    --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: 217.2 91.2% 59.8%;
    --primary-foreground: 222.2 84% 4.9%;
    --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: 224.3 76.3% 94.1%;
  }
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}

使用 next-themes 实现暗色模式

对于生产就绪的暗色模式实现,使用 next-themes 来避免水合问题。

安装 next-themes

npm install next-themes

创建主题提供程序

创建 src/components/theme-provider.tsx

"use client"

import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

更新您的根布局

修改 src/app/layout.tsx

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

创建主题切换组件

"use client"

import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"

export function ThemeToggle() {
  const { setTheme, theme } = useTheme()

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
      <span className="sr-only">切换主题</span>
    </Button>
  )
}

处理 React 19 的变化

React 19 引入了几个影响您使用 Shadcn 组件方式的变化:

简化的 ref 处理

React 19 允许将 ref 作为常规 prop 传递,在许多情况下消除了对 forwardRef 的需要:

// 旧方法(React 18)
const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, ...props }, ref) => {
    return <input ref={ref} className={className} {...props} />
  }
)

// 新方法(React 19)
function Input({ className, ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} className={className} {...props} />
}

新的表单 Hooks

React 19 的 useActionStateuseFormStatus hooks 与 Shadcn 表单组件无缝配合:

import { useActionState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"

function ContactForm() {
  const [state, formAction] = useActionState(async (prevState, formData) => {
    // 服务器操作逻辑
    const email = formData.get("email")
    // 处理表单...
    return { success: true }
  }, { success: false })

  return (
    <form action={formAction}>
      <Input name="email" type="email" required />
      <Button type="submit">提交</Button>
      {state.success && <p>感谢您的订阅!</p>}
    </form>
  )
}

常见问题和解决方案

Tailwind CSS 构建错误

如果在设置后遇到构建错误,请确保您的 PostCSS 配置正确:

// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

组件样式未应用

检查您的 globals.css 是否在根布局中导入:

import "./globals.css"

组件属性的 TypeScript 错误

确保您的 tsconfig.json 包含正确的路径:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

生产环境最佳实践

  1. 自定义 cn 实用程序lib/utils.ts 中的 cn 函数智能地合并类。根据您的特定需求扩展它。

  2. 创建组件变体:使用 CVA(class-variance-authority)在您的应用程序中创建一致的组件变体。

  3. 优化包大小:只安装您实际使用的组件。每个组件都是独立的。

  4. 测试可访问性:Shadcn 组件使用处理可访问性的 Radix UI 原语,但始终使用屏幕阅读器进行测试。

  5. 版本控制:由于组件被复制到您的项目中,请将它们提交到版本控制并跟踪更改。

结论

使用 Next.js 设置 Shadcn UI 为您提供了一个平衡灵活性和开发速度的现代组件系统。与传统组件库不同,您拥有每一行代码,可以自定义任何内容,并且不会被锁定在别人的设计决策中。

Next.js 15 的 App Router、React 19 的改进和 Tailwind CSS 的结合为构建现代 Web 应用程序创建了强大的基础。通过 Shadcn 复制组件而不是将其作为依赖项安装的方法,您可以获得两全其美:快速开发和完全控制。

常见问题

使用 npm 和 pnpm 安装 Shadcn UI 有什么区别?

在 React 19 中使用 npm 时,由于对等依赖冲突,您需要在组件安装期间使用 --legacy-peer-deps 标志。像 pnpm、yarn 和 bun 这样的包管理器更优雅地处理这些依赖项,不需要任何特殊标志。无论您选择哪个包管理器,最终结果都是相同的。

我可以在 Next.js 的 Pages Router 中使用 Shadcn UI 吗?

是的,Shadcn UI 与 App Router 和 Pages Router 都兼容。组件代码本身与框架无关。主要区别在于如何实现提供程序(如主题提供程序)- 在 Pages Router 中,您需要在 _app.tsx 而不是根布局中包装您的应用。

如何自定义默认的 Shadcn 组件样式?

由于 Shadcn 将组件直接复制到您的项目中,您可以像修改任何其他代码一样修改它们。打开 src/components/ui/ 中的组件文件,调整 Tailwind 类或 CVA 变体,然后保存。您还可以修改 CSS 中的全局主题变量,以一次性更改所有组件的颜色和间距。

我需要一次安装所有 Shadcn 组件吗?

不,您应该只在需要时安装组件。每个组件都是独立的,包含自己的依赖项。这保持了您的包大小最小化和项目组织良好。在需要时使用 npx shadcn@latest add [component-name] 添加单个组件。

当 Shadcn UI 更新组件时会发生什么?

由于组件被复制到您的项目中,当 Shadcn 发布新版本时,它们不会自动更新。您可以手动检查 Shadcn UI 文档的更新,然后重新运行特定组件的 add 命令或手动应用更改。这让您可以控制何时以及如何在项目中更新组件。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.