ByteNoteByteNote

字节笔记本

2026年7月20日

Next.js + Tailwind CSS + shadcn/ui:搭建现代前端项目与实现深色模式

API中转
¥120

Next.js + Tailwind CSS + shadcn/ui:搭建现代前端项目与实现深色模式

Next.js、Tailwind CSS 和 shadcn/ui 的组合是目前 React 生态中比较主流的技术选型。本文从项目初始化开始,一步步搭建一个支持深色模式切换的现代前端项目。

项目初始化

创建 Next.js 项目

bash
npx create-next-app@latest my-app --typescript --tailwind --eslint
cd my-app

创建时选择 TypeScript、Tailwind CSS 和 ESLint,项目会自动完成基础配置。

安装 shadcn/ui

bash
npx shadcn@latest init

初始化时会提示几个配置选项:

  • 样式风格:New York
  • 基础颜色:Zinc
  • 是否使用 CSS 变量:yes

配置完成后,components.json 文件会生成在项目根目录,后续添加组件时会用到。

bash
# 添加需要的组件
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dropdown-menu

推荐的项目结构

text
my-app/
├── app/
│   ├── layout.tsx
│   └── page.tsx
├── components/
│   ├── ui/              # shadcn/ui 组件(自动生成)
│   └── custom/          # 业务组件
├── lib/
│   └── utils.ts         # cn() 等工具函数
└── types/

实现深色模式

深色模式是现代应用的标配功能。使用 next-themes 库可以比较方便地实现。

安装依赖

bash
npm install next-themes

配置 Tailwind CSS

javascript
// tailwind.config.ts
const config = {
  darkMode: 'class',
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

关键配置是 darkMode: 'class',表示通过在 <html> 标签上切换 dark class 来控制深色模式。

创建 ThemeProvider

tsx
// app/providers.tsx
'use client'

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>
}

在根布局中挂载

tsx
// app/layout.tsx
import { ThemeProvider } from '@/app/providers'

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

几个配置项说明:

  • attribute="class":使用 class 方式切换主题
  • defaultTheme="system":默认跟随系统设置
  • enableSystem:启用系统主题检测
  • disableTransitionOnChange:切换时禁用过渡动画,避免闪烁
  • suppressHydrationWarning:防止服务端渲染和客户端不一致的警告

创建主题切换组件

tsx
// components/theme-toggle.tsx
'use client'

import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
import { Button } from '@/components/ui/button'
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'

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

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">切换主题</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme('light')}>
          浅色模式
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme('dark')}>
          深色模式
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme('system')}>
          跟随系统
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

太阳和月亮图标的切换用了 CSS transform 动画,通过 dark: 前缀控制显示隐藏。

在页面中使用

tsx
// components/nav-bar.tsx
import { ThemeToggle } from '@/components/theme-toggle'
import { Button } from '@/components/ui/button'

export function NavBar() {
  return (
    <nav className="flex items-center justify-between p-4 bg-white dark:bg-gray-900">
      <div className="text-xl font-bold dark:text-white">我的应用</div>
      <div className="flex gap-4 items-center">
        <ThemeToggle />
        <Button variant="outline">登录</Button>
      </div>
    </nav>
  )
}

样式适配要点

组件级别的深色适配

dark: 前缀为每个需要变化的元素指定深色样式:

tsx
<div className="bg-white dark:bg-gray-900 min-h-screen">
  <h1 className="text-gray-900 dark:text-white">标题</h1>
  <p className="text-gray-600 dark:text-gray-300">正文内容</p>
</div>

CSS 变量方式(shadcn/ui 推荐)

shadcn/ui 使用 CSS 变量管理主题色,在 globals.css 中定义:

css
@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 240 10% 3.9%;
    --card: 0 0% 100%;
    --card-foreground: 240 10% 3.9%;
    --primary: 240 5.9% 10%;
    --primary-foreground: 0 0% 98%;
    /* ...更多变量 */
  }

  .dark {
    --background: 240 10% 3.9%;
    --foreground: 0 0% 98%;
    --card: 240 10% 3.9%;
    --card-foreground: 0 0% 98%;
    --primary: 0 0% 98%;
    --primary-foreground: 240 5.9% 10%;
    /* ...更多变量 */
  }
}

用 CSS 变量的好处是修改一处,所有引用该变量的组件都会跟着变化,不需要逐个修改。

防止页面闪烁

页面首次加载时,如果深色模式还没设置好就渲染了浅色页面,会看到一次闪烁。可以在 <head> 中加一段内联脚本提前设置:

tsx
// app/layout.tsx
<html lang="zh-CN" suppressHydrationWarning>
  <head>
    <script dangerouslySetInnerHTML={{
      __html: `
        try {
          const theme = localStorage.getItem('theme')
          if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
            document.documentElement.classList.add('dark')
          }
        } catch (_) {}
      `
    }} />
  </head>
  <body>...</body>
</html>

这段脚本在页面渲染前执行,确保 <html> 标签在首次绘制时就有正确的 class。

实战示例:响应式仪表盘

tsx
// app/dashboard/page.tsx
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"

export default function DashboardPage() {
  return (
    <div className="p-8">
      <h1 className="text-3xl font-bold mb-6 text-gray-900 dark:text-white">
        仪表盘
      </h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        <Card>
          <CardHeader>
            <CardTitle>总用户</CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-2xl font-bold">1,234</p>
          </CardContent>
        </Card>
        <Card>
          <CardHeader>
            <CardTitle>日活跃</CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-2xl font-bold">567</p>
          </CardContent>
        </Card>
        <Card>
          <CardHeader>
            <CardTitle>转化率</CardTitle>
          </CardHeader>
          <CardContent>
            <p className="text-2xl font-bold">12.3%</p>
          </CardContent>
        </Card>
      </div>
    </div>
  )
}

shadcn/ui 的 Card 组件已经内置了对 CSS 变量的支持,所以深色模式下无需额外适配。

Tailwind CSS 响应式设计

Tailwind 的响应式断点是移动优先的,从小到大依次为:

前缀最小宽度适用设备
(无)0px手机
sm640px大屏手机
md768px平板
lg1024px笔记本
xl1280px桌面
2xl1536px大屏桌面
tsx
// 同一个元素在不同断点显示不同列数
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
  {items.map(item => <Card key={item.id} />)}
</div>

总结

这套技术栈的分工很清晰:Next.js 负责路由和渲染,Tailwind CSS 处理样式,shadcn/ui 提供基础组件。深色模式通过 next-themes + Tailwind 的 dark: 前缀实现,shadcn/ui 的 CSS 变量机制让主题切换更加统一。实际开发中,建议优先使用 shadcn/ui 提供的组件,减少重复造轮子,同时利用 Tailwind 的工具类做布局和细节调整。

分享: