本章目标:在 Next.js App Router 项目中建立一套完整的 Tailwind CSS 工程体系——涵盖安装配置、Design Token 设计、主题定制、暗色模式、响应式策略、自定义插件,以及与 shadcn/ui 的协作模式。
13.1 为什么选择 Tailwind CSS
CSS 方案对比
| 方案 | 类型 | 包体积 | 学习曲线 | 与 Next.js 适配 |
|---|---|---|---|---|
| Tailwind CSS | Utility-first | 极小(PurgeCSS) | 中 | ✅ 原生支持 |
| CSS Modules | Scoped CSS | 中等 | 低 | ✅ 原生支持 |
| Styled Components | CSS-in-JS | 较大(运行时) | 中 | ⚠️ 需 "use client" |
| vanilla-extract | Zero-runtime CSS-in-JS | 小 | 中 | ✅ 编译时 |
| Panda CSS | Zero-runtime Utility | 小 | 中 | ✅ 编译时 |
Tailwind CSS 在 Next.js 中的优势
- 零运行时开销:编译时生成 CSS,不增加 JS bundle
- 与 RSC 完美兼容:不需要
"use client",Server Component 直接使用 - 设计系统一致性:通过 Design Token 统一间距、颜色、字体
- JIT 编译器:按需生成,生产包极小
- 生态丰富:shadcn/ui、Headless UI、Radix UI 均基于 Tailwind
13.2 安装与配置
通过 create-next-app 安装(推荐)
npx create-next-app@latest my-app --tailwind --typescript --app --eslint
手动安装
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
配置文件
// tailwind.config.ts
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./lib/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [],
};
export default config;
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
在根布局中引入:
// app/layout.tsx
import './globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>{children}</body>
</html>
);
}
13.3 Design Token 体系
什么是 Design Token?
Design Token 是设计系统的原子化变量——颜色、字体、间距、圆角等基础值。Tailwind 的 theme.extend 就是 Token 的载体。
完整 Token 配置
// tailwind.config.ts
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
// ============ 颜色 ============
colors: {
// 品牌色
brand: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6', // 主色
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
// 语义色
success: {
light: '#d1fae5',
DEFAULT: '#10b981',
dark: '#065f46',
},
warning: {
light: '#fef3c7',
DEFAULT: '#f59e0b',
dark: '#92400e',
},
danger: {
light: '#fee2e2',
DEFAULT: '#ef4444',
dark: '#991b1b',
},
// 中性色
surface: {
DEFAULT: '#ffffff',
secondary: '#f9fafb',
tertiary: '#f3f4f6',
},
},
// ============ 字体 ============
fontFamily: {
sans: [
'Inter',
'PingFang SC',
'Microsoft YaHei',
'sans-serif',
],
mono: [
'JetBrains Mono',
'Fira Code',
'monospace',
],
display: [
'Cal Sans',
'Inter',
'sans-serif',
],
},
fontSize: {
'2xs': ['0.625rem', { lineHeight: '0.875rem' }],
'xs': ['0.75rem', { lineHeight: '1rem' }],
'sm': ['0.875rem', { lineHeight: '1.25rem' }],
'base': ['1rem', { lineHeight: '1.5rem' }],
'lg': ['1.125rem', { lineHeight: '1.75rem' }],
'xl': ['1.25rem', { lineHeight: '1.75rem' }],
'2xl': ['1.5rem', { lineHeight: '2rem' }],
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
'4xl': ['2.25rem', { lineHeight: '2.5rem' }],
'5xl': ['3rem', { lineHeight: '1.16' }],
'6xl': ['3.75rem', { lineHeight: '1.1' }],
},
// ============ 间距 ============
spacing: {
'18': '4.5rem',
'22': '5.5rem',
'30': '7.5rem',
'128': '32rem',
'144': '36rem',
},
// ============ 圆角 ============
borderRadius: {
'4xl': '2rem',
'5xl': '2.5rem',
},
// ============ 阴影 ============
boxShadow: {
'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
'medium': '0 4px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 30px -5px rgba(0, 0, 0, 0.04)',
'strong': '0 10px 40px -10px rgba(0, 0, 0, 0.2)',
'glow': '0 0 20px rgba(59, 130, 246, 0.3)',
},
// ============ 动画 ============
animation: {
'fade-in': 'fadeIn 0.5s ease-out',
'slide-up': 'slideUp 0.5s ease-out',
'slide-down': 'slideDown 0.3s ease-out',
'scale-in': 'scaleIn 0.2s ease-out',
'spin-slow': 'spin 3s linear infinite',
'pulse-soft': 'pulseSoft 2s ease-in-out infinite',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
slideDown: {
'0%': { opacity: '0', transform: 'translateY(-10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
scaleIn: {
'0%': { opacity: '0', transform: 'scale(0.95)' },
'100%': { opacity: '1', transform: 'scale(1)' },
},
pulseSoft: {
'0%, 100%': { opacity: '1' },
'50%': { opacity: '0.7' },
},
},
// ============ 容器 ============
containers: {
'2xs': '16rem',
},
// ============ 过渡 ============
transitionDuration: {
'400': '400ms',
'2000': '2000ms',
},
},
},
plugins: [],
};
export default config;
使用方式
// 品牌色
<h1 className="text-brand-500">Hello</h1>
<button className="bg-brand-600 hover:bg-brand-700 text-white">Submit</button>
// 语义色
<div className="bg-success-light text-success-dark">操作成功</div>
<div className="bg-danger-light text-danger-dark">操作失败</div>
// 自定义字体
<h1 className="font-display text-5xl">大标题</h1>
<code className="font-mono text-sm">const x = 1;</code>
// 自定义阴影
<div className="shadow-soft p-6 rounded-xl">卡片</div>
// 自定义动画
<div className="animate-fade-in">淡入内容</div>
<div className="animate-slide-up">上滑内容</div>
13.4 CSS 变量与主题切换
定义 CSS 变量
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* 亮色主题 */
--color-background: 255 255 255;
--color-foreground: 15 23 42;
--color-card: 255 255 255;
--color-card-foreground: 15 23 42;
--color-popover: 255 255 255;
--color-popover-foreground: 15 23 42;
--color-primary: 59 130 246;
--color-primary-foreground: 255 255 255;
--color-secondary: 241 245 249;
--color-secondary-foreground: 15 23 42;
--color-muted: 241 245 249;
--color-muted-foreground: 100 116 139;
--color-accent: 241 245 249;
--color-accent-foreground: 15 23 42;
--color-destructive: 239 68 68;
--color-destructive-foreground: 255 255 255;
--color-border: 226 232 240;
--color-input: 226 232 240;
--color-ring: 59 130 246;
--radius: 0.5rem;
}
.dark {
/* 暗色主题 */
--color-background: 15 23 42;
--color-foreground: 248 250 252;
--color-card: 30 41 59;
--color-card-foreground: 248 250 252;
--color-popover: 30 41 59;
--color-popover-foreground: 248 250 252;
--color-primary: 96 165 250;
--color-primary-foreground: 15 23 42;
--color-secondary: 51 65 85;
--color-secondary-foreground: 248 250 252;
--color-muted: 51 65 85;
--color-muted-foreground: 148 163 184;
--color-accent: 51 65 85;
--color-accent-foreground: 248 250 252;
--color-destructive: 239 68 68;
--color-destructive-foreground: 255 255 255;
--color-border: 51 65 85;
--color-input: 51 65 85;
--color-ring: 96 165 250;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
}
在 Tailwind 中映射 CSS 变量
// tailwind.config.ts
import type { Config } from 'tailwindcss';
const config: Config = {
darkMode: 'class',
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
background: 'rgb(var(--color-background) / <alpha-value>)',
foreground: 'rgb(var(--color-foreground) / <alpha-value>)',
card: {
DEFAULT: 'rgb(var(--color-card) / <alpha-value>)',
foreground: 'rgb(var(--color-card-foreground) / <alpha-value>)',
},
popover: {
DEFAULT: 'rgb(var(--color-popover) / <alpha-value>)',
foreground: 'rgb(var(--color-popover-foreground) / <alpha-value>)',
},
primary: {
DEFAULT: 'rgb(var(--color-primary) / <alpha-value>)',
foreground: 'rgb(var(--color-primary-foreground) / <alpha-value>)',
},
secondary: {
DEFAULT: 'rgb(var(--color-secondary) / <alpha-value>)',
foreground: 'rgb(var(--color-secondary-foreground) / <alpha-value>)',
},
muted: {
DEFAULT: 'rgb(var(--color-muted) / <alpha-value>)',
foreground: 'rgb(var(--color-muted-foreground) / <alpha-value>)',
},
accent: {
DEFAULT: 'rgb(var(--color-accent) / <alpha-value>)',
foreground: 'rgb(var(--color-accent-foreground) / <alpha-value>)',
},
destructive: {
DEFAULT: 'rgb(var(--color-destructive) / <alpha-value>)',
foreground: 'rgb(var(--color-destructive-foreground) / <alpha-value>)',
},
border: 'rgb(var(--color-border) / <alpha-value>)',
input: 'rgb(var(--color-input) / <alpha-value>)',
ring: 'rgb(var(--color-ring) / <alpha-value>)',
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
},
},
plugins: [
require('tailwindcss-animate'),
],
};
export default config;
使用语义化 Token
// ❌ 不要硬编码颜色
<div className="bg-white text-gray-900 dark:bg-slate-800 dark:text-white">
Hello
</div>
// ✅ 使用语义化 Token
<div className="bg-card text-card-foreground">
Hello
</div>
// ✅ 主色按钮
<button className="bg-primary text-primary-foreground hover:bg-primary/90">
提交
</button>
// ✅ 次要按钮
<button className="bg-secondary text-secondary-foreground hover:bg-secondary/80">
取消
</button>
13.5 暗色模式
三种切换策略
1. 系统偏好(默认)
// tailwind.config.ts
const config: Config = {
darkMode: 'media', // 根据 prefers-color-scheme
};
<div className="bg-white dark:bg-slate-900">
<p className="text-gray-900 dark:text-gray-100">
自动跟随系统暗色模式
</p>
</div>
2. 手动切换(class 策略)
// tailwind.config.ts
const config: Config = {
darkMode: 'class', // 根据 html 元素的 class
};
// app/components/ThemeToggle.tsx
'use client';
import { useEffect, useState } from 'react';
type Theme = 'light' | 'dark' | 'system';
export function ThemeToggle() {
const [theme, setTheme] = useState<Theme>('system');
useEffect(() => {
// 从 localStorage 读取
const stored = localStorage.getItem('theme') as Theme | null;
if (stored) {
setTheme(stored);
}
}, []);
useEffect(() => {
const root = document.documentElement;
if (theme === 'system') {
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
root.classList.toggle('dark', systemDark);
} else {
root.classList.toggle('dark', theme === 'dark');
}
localStorage.setItem('theme', theme);
}, [theme]);
return (
<div className="flex items-center gap-1 p-1 bg-muted rounded-lg">
<button
onClick={() => setTheme('light')}
className={`px-3 py-1.5 rounded-md text-sm transition-colors ${
theme === 'light' ? 'bg-background shadow-sm' : 'hover:bg-background/50'
}`}
>
☀️
</button>
<button
onClick={() => setTheme('dark')}
className={`px-3 py-1.5 rounded-md text-sm transition-colors ${
theme === 'dark' ? 'bg-background shadow-sm' : 'hover:bg-background/50'
}`}
>
🌙
</button>
<button
onClick={() => setTheme('system')}
className={`px-3 py-1.5 rounded-md text-sm transition-colors ${
theme === 'system' ? 'bg-background shadow-sm' : 'hover:bg-background/50'
}`}
>
💻
</button>
</div>
);
}
3. 防止闪烁(FOUC)
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<head>
{/* 内联脚本:在 CSS 解析前设置 dark class */}
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
} catch (e) {}
})();
`,
}}
/>
</head>
<body>{children}</body>
</html>
);
}
使用 next-themes(推荐方案)
npm install next-themes
// app/providers.tsx
'use client';
import { ThemeProvider } from 'next-themes';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
);
}
// app/components/ThemeToggle.tsx
'use client';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) {
return <div className="w-[120px] h-[36px]" />; // 占位
}
return (
<select
value={theme}
onChange={(e) => setTheme(e.target.value)}
className="px-3 py-1.5 bg-muted rounded-lg text-sm"
>
<option value="light">浅色</option>
<option value="dark">深色</option>
<option value="system">系统</option>
</select>
);
}
13.6 响应式设计策略
断点系统
// tailwind.config.ts
const config: Config = {
theme: {
screens: {
'xs': '475px', // 额外添加的超小屏断点
'sm': '640px', // 手机横屏
'md': '768px', // 平板
'lg': '1024px', // 小笔记本
'xl': '1280px', // 桌面
'2xl': '1536px', // 大屏
},
},
};
Mobile-First 实践
// ❌ Desktop-First(不推荐)
<div className="grid grid-cols-4 max-md:grid-cols-2 max-sm:grid-cols-1">
// ✅ Mobile-First(推荐)
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
容器查询(Container Queries)
npm install -D @tailwindcss/container-queries
// tailwind.config.ts
plugins: [
require('@tailwindcss/container-queries'),
],
// 父容器声明
<div className="@container">
{/* 子元素根据父容器宽度响应 */}
<div className="grid grid-cols-1 @md:grid-cols-2 @lg:grid-cols-3">
<Card />
<Card />
<Card />
</div>
</div>
响应式排版
/* app/globals.css */
@layer components {
.prose-responsive {
@apply prose
prose-sm
md:prose-base
lg:prose-lg
prose-headings:font-display
prose-headings:tracking-tight
prose-a:text-primary
prose-a:no-underline
hover:prose-a:underline
prose-img:rounded-lg
prose-code:font-mono
prose-code:text-sm
dark:prose-invert;
}
}
13.7 @apply 与组件类
使用 @apply 提取公共样式
/* app/globals.css */
@layer components {
/* 按钮变体 */
.btn {
@apply inline-flex items-center justify-center
rounded-lg font-medium
transition-colors duration-200
focus:outline-none focus:ring-2 focus:ring-offset-2
disabled:opacity-50 disabled:pointer-events-none;
}
.btn-primary {
@apply btn bg-primary text-primary-foreground
hover:bg-primary/90
focus:ring-primary;
}
.btn-secondary {
@apply btn bg-secondary text-secondary-foreground
hover:bg-secondary/80
focus:ring-secondary;
}
.btn-outline {
@apply btn border border-input bg-background
hover:bg-accent hover:text-accent-foreground
focus:ring-ring;
}
.btn-ghost {
@apply btn hover:bg-accent hover:text-accent-foreground;
}
.btn-destructive {
@apply btn bg-destructive text-destructive-foreground
hover:bg-destructive/90
focus:ring-destructive;
}
/* 按钮尺寸 */
.btn-sm {
@apply h-8 px-3 text-xs;
}
.btn-md {
@apply h-10 px-4 text-sm;
}
.btn-lg {
@apply h-12 px-6 text-base;
}
/* 输入框 */
.input {
@apply flex h-10 w-full rounded-lg border border-input
bg-background px-3 py-2 text-sm
placeholder:text-muted-foreground
focus:outline-none focus:ring-2 focus:ring-ring
disabled:cursor-not-allowed disabled:opacity-50;
}
/* 卡片 */
.card {
@apply rounded-xl border border-border bg-card text-card-foreground
shadow-soft;
}
.card-header {
@apply p-6 pb-0;
}
.card-content {
@apply p-6;
}
.card-footer {
@apply p-6 pt-0;
}
/* Badge */
.badge {
@apply inline-flex items-center rounded-full px-2.5 py-0.5
text-xs font-medium transition-colors;
}
.badge-primary {
@apply badge bg-primary/10 text-primary;
}
.badge-success {
@apply badge bg-success-light text-success-dark;
}
}
使用方式
<button className="btn-primary btn-md">提交</button>
<button className="btn-outline btn-md">取消</button>
<button className="btn-ghost btn-sm">更多</button>
<input className="input" placeholder="请输入..." />
<div className="card">
<div className="card-header">
<h3 className="text-lg font-semibold">标题</h3>
</div>
<div className="card-content">
<p>内容</p>
</div>
<div className="card-footer">
<button className="btn-primary btn-sm">操作</button>
</div>
</div>
@apply vs React 组件
适用 @apply 的场景:
✅ 全局基础样式(body、h1-h6、a 标签)
✅ 简单的 CSS 类组合
✅ 第三方库样式覆盖
适用 React 组件的场景:
✅ 需要 props 控制变体(size、variant)
✅ 需要条件渲染逻辑
✅ 需要组合多个子组件
✅ 需要事件处理
13.8 Tailwind 插件
常用插件
# 动画插件
npm install -D tailwindcss-animate
# 容器查询
npm install -D @tailwindcss/container-queries
# 排版(prose)
npm install -D @tailwindcss/typography
# 表单样式
npm install -D @tailwindcss/forms
# 纵横比
npm install -D @tailwindcss/aspect-ratio
// tailwind.config.ts
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
],
darkMode: 'class',
theme: {
extend: {
// ...
},
},
plugins: [
require('tailwindcss-animate'),
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
require('@tailwindcss/container-queries'),
require('@tailwindcss/aspect-ratio'),
],
};
export default config;
排版插件使用
// 文章渲染
<article className="prose prose-lg dark:prose-invert max-w-none">
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
// 自定义 prose 变体
<article className="prose
prose-headings:font-display
prose-headings:tracking-tight
prose-a:text-primary
prose-a:no-underline
hover:prose-a:underline
prose-code:font-mono
prose-code:before:content-['']
prose-code:after:content-['']
prose-img:rounded-xl
dark:prose-invert
">
<div dangerouslySetInnerHTML={{ __html: content }} />
</article>
自定义插件
// tailwind.config.ts
const plugin = require('tailwindcss/plugin');
const config: Config = {
plugins: [
// 文本渐变插件
plugin(function ({ addUtilities, theme }) {
addUtilities({
'.text-gradient': {
'background-clip': 'text',
'-webkit-background-clip': 'text',
'-webkit-text-fill-color': 'transparent',
},
'.text-gradient-primary': {
'background-image': `linear-gradient(to right, ${theme('colors.brand.400')}, ${theme('colors.brand.600')})`,
'background-clip': 'text',
'-webkit-background-clip': 'text',
'-webkit-text-fill-color': 'transparent',
},
});
}),
// Glass 效果插件
plugin(function ({ addUtilities }) {
addUtilities({
'.glass': {
'background': 'rgba(255, 255, 255, 0.1)',
'backdrop-filter': 'blur(10px)',
'-webkit-backdrop-filter': 'blur(10px)',
'border': '1px solid rgba(255, 255, 255, 0.2)',
},
'.glass-dark': {
'background': 'rgba(0, 0, 0, 0.2)',
'backdrop-filter': 'blur(10px)',
'-webkit-backdrop-filter': 'blur(10px)',
'border': '1px solid rgba(255, 255, 255, 0.1)',
},
});
}),
// 滚动条隐藏
plugin(function ({ addUtilities }) {
addUtilities({
'.scrollbar-hide': {
'-ms-overflow-style': 'none',
'scrollbar-width': 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
},
});
}),
],
};
使用方式:
<h1 className="text-4xl font-bold text-gradient-primary">
渐变标题
</h1>
<div className="glass p-6 rounded-xl">
毛玻璃效果
</div>
<div className="overflow-x-auto scrollbar-hide">
<div className="flex gap-4">
{/* 横向滚动内容 */}
</div>
</div>
13.9 与 shadcn/ui 协作
shadcn/ui 初始化
npx shadcn@latest init
生成的配置:
// components.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"
}
}
安装组件
# 按需安装组件
npx shadcn@latest add button
npx shadcn@latest add input
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add dropdown-menu
npx shadcn@latest add table
npx shadcn@latest add toast
npx shadcn@latest add form
组件源码(完全可控)
// 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 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
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 };
cn 工具函数
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
npm install clsx tailwind-merge
使用 shadcn/ui 组件
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card';
export default function ContactForm() {
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>联系我们</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Input placeholder="您的姓名" />
<Input type="email" placeholder="邮箱地址" />
</CardContent>
<CardFooter>
<Button>发送</Button>
<Button variant="outline" className="ml-2">取消</Button>
</CardFooter>
</Card>
);
}
13.10 性能优化
生产环境 CSS 瘦身
Tailwind v3 内置 JIT(Just-In-Time)编译器,只生成你使用到的 CSS 类:
# 开发环境:动态编译
npm run dev
# 生产构建:自动 tree-shaking
npm run build
# 输出 CSS 通常 < 10KB(gzip 后)
避免常见性能陷阱
// ❌ 动态拼接类名(JIT 无法检测)
const color = 'blue';
<div className={`bg-${color}-500`}>
// ✅ 使用完整类名
<div className="bg-blue-500">
// ✅ 或使用 safelist(不得已时)
// tailwind.config.ts
safelist: [
'bg-blue-500',
'bg-green-500',
'bg-red-500',
],
减少重复样式
// ❌ 在每个组件上重复写相同的类
<div className="flex items-center justify-center rounded-lg bg-card p-6 shadow-soft">
<div className="flex items-center justify-center rounded-lg bg-card p-6 shadow-soft">
// ✅ 提取为组件或 @apply
<div className="card card-content flex items-center justify-center">
图片优化
// 使用 Next.js Image 组件 + Tailwind
import Image from 'next/image';
<div className="relative aspect-video overflow-hidden rounded-xl">
<Image
src="/hero.jpg"
alt="Hero"
fill
className="object-cover"
priority
/>
</div>
13.11 实战:构建博客首页
// app/page.tsx
import Link from 'next/link';
import { getArticles } from '@/lib/services/article';
import { ThemeToggle } from '@/app/components/ThemeToggle';
export default async function HomePage() {
const { articles } = await getArticles({ limit: 6, featured: true });
return (
<div className="min-h-screen bg-background">
{/* 导航栏 */}
<nav className="sticky top-0 z-50 border-b border-border bg-background/80 backdrop-blur-lg">
<div className="max-w-6xl mx-auto flex items-center justify-between px-4 h-16">
<Link href="/" className="text-xl font-display font-bold text-gradient-primary">
MyBlog
</Link>
<div className="flex items-center gap-6">
<Link href="/articles" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
文章
</Link>
<Link href="/about" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
关于
</Link>
<ThemeToggle />
</div>
</div>
</nav>
{/* Hero 区域 */}
<section className="relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-brand-500/10 via-transparent to-brand-600/10" />
<div className="relative max-w-6xl mx-auto px-4 py-24 md:py-32 text-center">
<h1 className="text-4xl md:text-6xl font-display font-bold tracking-tight animate-fade-in">
探索 <span className="text-gradient-primary">Next.js</span> 的无限可能
</h1>
<p className="mt-6 text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto animate-slide-up">
从基础到生产实战,系统学习 App Router、Server Components、Server Actions 等最新技术。
</p>
<div className="mt-8 flex items-center justify-center gap-4 animate-slide-up">
<Link
href="/articles"
className="btn-primary btn-lg"
>
开始阅读
</Link>
<Link
href="/about"
className="btn-outline btn-lg"
>
了解更多
</Link>
</div>
</div>
</section>
{/* 文章列表 */}
<section className="max-w-6xl mx-auto px-4 py-16">
<div className="flex items-center justify-between mb-8">
<h2 className="text-2xl font-bold">精选文章</h2>
<Link href="/articles" className="text-sm text-primary hover:underline">
查看全部 →
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{articles.map((article, i) => (
<Link
key={article.id}
href={`/articles/${article.slug}`}
className="group card hover:shadow-medium transition-all duration-300"
style={{ animationDelay: `${i * 100}ms` }}
>
{/* 封面图 */}
<div className="aspect-video overflow-hidden rounded-t-xl">
<div className="w-full h-full bg-gradient-to-br from-brand-400 to-brand-600 group-hover:scale-105 transition-transform duration-300" />
</div>
{/* 内容 */}
<div className="p-5">
<div className="flex items-center gap-2 mb-3">
{article.category && (
<span className="badge-primary">{article.category}</span>
)}
<time className="text-2xs text-muted-foreground">
{new Date(article.createdAt).toLocaleDateString('zh-CN')}
</time>
</div>
<h3 className="font-semibold text-lg group-hover:text-primary transition-colors line-clamp-2">
{article.title}
</h3>
<p className="mt-2 text-sm text-muted-foreground line-clamp-2">
{article.excerpt}
</p>
<div className="mt-4 flex items-center gap-4 text-xs text-muted-foreground">
<span>{article._count.comments} 评论</span>
<span>{article.views} 阅读</span>
</div>
</div>
</Link>
))}
</div>
</section>
{/* 底部 */}
<footer className="border-t border-border bg-surface-secondary">
<div className="max-w-6xl mx-auto px-4 py-12">
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
<p className="text-sm text-muted-foreground">
© 2025 MyBlog. All rights reserved.
</p>
<div className="flex items-center gap-6">
<Link href="/privacy" className="text-sm text-muted-foreground hover:text-foreground">
隐私政策
</Link>
<Link href="/terms" className="text-sm text-muted-foreground hover:text-foreground">
使用条款
</Link>
</div>
</div>
</div>
</footer>
</div>
);
}
本章小结
Key Takeaways
- Tailwind CSS 是 Next.js 的最佳 CSS 方案:零运行时、RSC 兼容、JIT 编译
- Design Token 是设计系统的基础:通过 CSS 变量 + Tailwind 映射实现主题切换
- 暗色模式有三种策略:系统偏好、手动切换、
next-themes方案(推荐) - Mobile-First 是响应式设计的正确方式:从小屏开始,逐步增强
- shadcn/ui 是 Tailwind 生态的最佳组件库:源码可控、完全可定制
- 性能关键在于 JIT:只生成使用到的类,避免动态拼接类名
下一步
下一章我们将深入 组件库选型与实战——对比 shadcn/ui、MUI、Ant Design 在 Next.js 中的适配方式,并构建一套完整的业务组件体系。
参考资料
- Tailwind CSS 官方文档
- Tailwind CSS + Next.js
- next-themes
- shadcn/ui
- class-variance-authority
- tailwind-merge
- @tailwindcss/typography
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。