Next.js 样式方案
全局 CSS、CSS Modules 与 Tailwind 的组织与取舍,以及 CSS-in-JS 在服务器组件下的边界
知识点地图
- 知识类别:Next.js(App Router)下的样式组织——全局样式、CSS Modules、Tailwind 与 CSS-in-JS。
- 解决什么问题:样式写在哪、怎么不互相污染、怎么和服务器/客户端组件边界共存;新项目选型与存量项目混用时的判断依据。
- 什么时候用到:新项目初始化定样式方案、接现成组件库、做主题换肤、排查”样式互相覆盖”或”styled-components 在服务器组件里报错”。
- 本篇只讲样式的组织方式;图片字体等资源优化见《静态资源与性能优化》,主题令牌与设计系统的思路见 006-css 模块。
0. 一句话理解
Next.js 官方推荐的三层样式结构:真正全局的(重置、令牌、Tailwind 基础层)进根布局的全局 CSS;组件级样式用 Tailwind 工具类为主、CSS Modules 兜底;CSS-in-JS 库在 App Router 下必须包在客户端边界里——样式体系本身没有运行时开销才是首选。
1. 全局 CSS:只放”真正全局”的东西
/* app/globals.css */
:root {
--color-brand: #2563eb;
--font-sans: var(--font-inter), system-ui, sans-serif;
}
body {
margin: 0;
font-family: var(--font-sans);
}// app/layout.tsx —— 根布局导入,作用于所有路由
import "./globals.css"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>{children}</body>
</html>
)
}讲解:
- 全局 CSS 在根布局导入一次即可作用于全站;它适合放三样东西:样式重置(reset/normalize)、CSS 变量令牌(颜色/字体/间距)、以及 Tailwind 这类框架的基础层。
- 官方文档明确提醒:全局样式不要用来写具体组件的样式。原因在 React 19 的样式表集成机制下,路由切换时样式表不会按路由卸载,两个页面各自在全局 CSS 里写
.header会静默互相覆盖——这是”改了 A 页样式 B 页崩了”的最常见来源。 - 易错点:任何组件文件里都能 import 全局 CSS(不止根布局),但顺序决定覆盖关系——生产构建会按 import 顺序合并 CSS 文件,所以”后 import 的赢”只在当前顺序下成立,跨文件调整 import 顺序就可能翻转样式。官方建议把全局样式的 import 收敛到根布局一处。
2. CSS Modules:类名自动隔离的组件样式
/* app/dashboard/table.module.css */
.table {
width: 100%;
border-collapse: collapse;
}
.table th {
background: var(--color-brand);
color: white;
}// app/dashboard/page.tsx(服务器组件里直接用)
import styles from "./table.module.css"
export default function DashboardPage() {
return (
<table className={styles.table}>
{/* ... */}
</table>
)
}讲解:
.module.css后缀是约定:构建器把文件里每个类名改写成”文件名_类名_随机哈希”的唯一名,styles.table拿到的就是改写后的真实类名。两个模块文件里都叫.table,最终 DOM 上也是两个不同类名——隔离是构建器给的,不是约定俗成的命名规范给的。- CSS Modules 在服务器组件中直接可用:它只是构建期类名替换 + 样式注入,没有任何运行时。这是它和 CSS-in-JS 的本质区别。
- 组合既有类用
composes;动态值不要往 CSS Modules 里塞(它没有插值能力),动态部分交给行内style或 CSS 变量:<div style={{ "--offset": offset }}>配合 var(--offset)。 - 易错点:
styles是对象,取不存在的键得到undefined,React 会静默忽略 className 里的 undefined——样式”没生效”先打印styles看键名,别急着怀疑构建。
3. Tailwind:脚手架默认,工具类为主线
create-next-app --tailwind(见《Next.js 16 快速上手》第 1 节)装好的 Tailwind v4 接法只有两处:
// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
}/* app/globals.css */
@import "tailwindcss";export default function Page() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<h1 className="text-4xl font-bold">Welcome to Next.js!</h1>
</main>
)
}讲解:
- Tailwind v4 与 v3 的接入方式不同:v4 不再要
tailwind.config.js与@tailwind三连指令,一个 PostCSS 插件加一行@import即可;需要兼容老浏览器时官方保留 v3 接法文档。 - 工具类写进
className,组件本身仍是普通组件——服务器组件里用 Tailwind 完全没有边界问题,这是官方把 Tailwind 列为首选样式的核心原因之一。 - 生产构建会按路由拆分并压缩 CSS,页面只加载它用到的规则;开发时配合 Fast Refresh 即时生效。注意开发与生产的 CSS 合并顺序可能有差异,上线前用
next build验证一次最终样式。 - 官方推荐的位置感:Tailwind 覆盖大部分场景;Tailwind 表达不了的复杂选择器/状态(如深层子元素、伪元素组合)用 CSS Modules;两者可以共存于同一组件。
4. CSS-in-JS:为什么必须包客户端边界
// components/themed-card.tsx
"use client" // 没有这一行,styled-components 直接在服务器组件里报错
import styled from "styled-components"
const Card = styled.div`
border: 1px solid var(--color-brand);
padding: 16px;
`
export function ThemedCard({ children }: { children: React.ReactNode }) {
return <Card>{children}</Card>
}// app/page.tsx —— 服务器组件引用客户端样式组件
import { ThemedCard } from "@/components/themed-card"
export default function Page() {
return <ThemedCard>你好</ThemedCard>
}讲解:
- styled-components 这类库的工作原理是”组件渲染时在 JS 运行时里生成样式并插进
<style>标签”。服务器组件不做 hydration、没有浏览器运行时,无法执行这套逻辑,所以官方文档明确:CSS-in-JS 库在 App Router 下需要标记"use client",由客户端渲染这些组件。 - 后果与代价:样式组件一旦标了
"use client",它的 children 仍可以是服务器组件传进来的(children 作为 props 穿边界),但组件自身的样式逻辑进入了客户端 bundle——这正是 RSC 世界里 CSS-in-JS 使用率下降的原因:为样式付出运行时体积,不如 Tailwind/CSS Modules 的零运行时方案。 - 两种务实姿势:a) 新代码优先 Tailwind + CSS Modules;b) 存量 styled-components 项目渐进迁移——把带样式的叶子组件收进客户端边界,页面骨架保持服务器组件,不要因为样式问题把整个页面改成
"use client"。 - 易错点:
"use client"边界上的样式组件丢失服务器端数据获取能力。若一个 styled 组件需要取数,正确结构是”服务器组件取数 -> 把数据作为 props 传给客户端样式组件”,而不是让样式组件自己取。
5. 三个真实场景
场景一:营销页主题色改造。 市场部要求品牌主色从蓝换成橙,且大促期间一键切换。原型代码里颜色硬编码在三十多个组件的 Tailwind 类里(bg-blue-600)。改造方案:全局 CSS 定义 --color-brand 令牌,tailwind 侧用 v4 的 @theme 把令牌映射成工具类,组件统一用 bg-brand 这类语义类名;大促切换只需在根布局换一组 CSS 变量。教训:主题色从第一天就该是变量,工具类里写具体色值等于把设计决策撒进每个文件。
场景二:后台管理表格的样式隔离。 老后台把 jQuery 时代的 table.css(.table { border: ... })全局引入后,新做的官网落地页表格样式全乱——全局 .table 类被两边互相覆盖。修复:把表格样式改写为 data-table.module.css,类名构建期自动加哈希,两边互不感知;全局 CSS 只保留 reset。这类”类名泄漏”事故在多人协作的全局 CSS 里必然发生,CSS Modules 的价值就在这里。
场景三:把现成 Tailwind 组件库接进 App Router。 团队选定 shadcn/ui(基于 Tailwind + Radix 的复制式组件库)。接入三步:按库文档初始化(本质是写入令牌到 globals.css + 安装依赖);复制需要的组件源码进 components/;在服务器组件页面里直接使用纯展示组件。注意点:库中依赖浏览器 API 的复合组件(Dialog、Dropdown)自带客户端标记,按第 4 节的边界规则使用即可;不要把整个组件库做成一个 "use client" 的大出口。
6. 动手实践
任务一:把全局样式收拾干净。 找一个(或造一个)在全局 CSS 里写了组件级类名的项目,把组件样式迁移到 CSS Modules,保留 reset 与令牌在全局。验证方式:路由 A、B 各有一个同名类名的组件,切换时互不影响。提示:迁移时先 grep 全局 CSS 里所有类名,逐个确认消费方,防止漏改。
任务二:体验 styled-components 的边界报错。 新建一个不写 "use client" 的 styled 组件并在服务器组件页面里使用,读一遍报错信息;加上 "use client" 修复,再把页面骨架的重心移回服务器组件(数据在服务器取、样式组件只管渲染)。提示:报错的关键词是事件处理器/浏览器 API 在服务器组件中不可用,与样式无关的错误经常从这里连锁出现。
任务三:语义令牌替换硬编码色值。 把一个页面里所有具体色值的 Tailwind 类(如 bg-blue-600)替换为基于 CSS 变量的语义类(如 bg-brand),并实现一个”暗色模式”开关切换令牌值。提示:Tailwind v4 用 @theme 定义令牌映射;开关只需切换根元素上的 class 或 data 属性,不必重渲染组件树。
先自己操作,再对照参考实现:
任务三参考实现
/* app/globals.css */
@import "tailwindcss";
@theme {
--color-brand: #2563eb; /* 语义令牌:亮色默认值 */
}
:root[data-theme="dark"] {
--color-brand: #f59e0b; /* 暗色下品牌色换档 */
}// app/theme-switcher.tsx
"use client"
export function ThemeSwitcher() {
return (
<button
className="rounded bg-brand px-3 py-1 text-white"
onClick={() => {
const root = document.documentElement
root.dataset.theme = root.dataset.theme === "dark" ? "" : "dark"
}}
>
切换主题
</button>
)
}要点:a) @theme 里定义的 --color-brand 会生成 bg-brand 工具类;b) 切换只改 data-theme 属性,颜色变化由 CSS 变量级联完成——按钮重渲染与否不影响其他组件;c) 服务器组件页面里直接放 <ThemeSwitcher />,边界只包这一个按钮。
7. 一句话记住
全局 CSS 只放重置与令牌;组件样式首选 Tailwind 工具类,复杂选择器用 CSS Modules;CSS-in-JS 在 App Router 下必须
"use client"且付出运行时代价——新项目没有理由把它当默认。
参考与致谢
- Next.js 官方文档 CSS 章节(Global CSS / CSS Modules / Tailwind / Ordering and Merging)与 CSS-in-JS 指南,来源:https://nextjs.org/docs/app/getting-started/css ,许可证 CC BY 4.0。本文接入代码形态与”样式表不随路由卸载”等注意事项依据 16 版官方文档整理。