CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Next.js 实战入门/课时/Tailwind 与样式
RawBlame
课时目录12 节

Next.js 实战入门

  1. 01Next.js 是什么
  2. 02项目创建与目录
  3. 03App Router 与文件路由
  4. 04Server 与 Client 组件
  5. 05数据获取与缓存
  6. 06渲染模式 SSR / SSG / ISR
  7. 07Route Handlers
  8. 08MDX 与课时渲染
  9. 09Tailwind 与样式
  10. 10认证与登录门控
  11. 11读 platform 源码
  12. 12综合练习与部署

Tailwind 与样式

globals.css、Tailwind v4、组件样式约定。

30 分钟

Tailwind 与样式

这一课你将能做到

  1. 用 Tailwind 工具类写出响应式、带暗色模式的卡片布局,理解「原子类」的组成规律
  2. 区分 Next.js 四种样式方案(Tailwind / CSS Modules / 全局 CSS / CSS-in-JS)并知道本项目选了哪种、为什么
  3. 用 clsx/cn 管理条件类名,把本站 gh-box 这类「语义组件类 + 工具类」的混合用法写对

为什么要学

样式在 Next.js 里不是小事,而是架构决策:选错方案,做出来的东西要么 SEO 差(CSS 加载晚导致页面裸奔)、要么包体积失控。本站 platform 选了 Tailwind,这不是跟风,看两个硬理由:

  1. Server 组件友好:RSC 的世界里「样式库不能带运行时」。styled-components / emotion 这类 CSS-in-JS 需要在浏览器里注入样式,和 Server 组件八字不合(要么报错要么强制转 Client)。Tailwind 是纯 CSS,没有运行时,Server/Client 组件通吃。
  2. 消灭「起名字」成本与死代码:传统写法要先给类起名 .card-wrapper,命名冲突、复用混乱、重构时一堆没人敢删的死 CSS。Tailwind 的类名即样式本体,没用到的类构建时自动剔除。

人话理解 Tailwind:它把 CSS 的每个属性拆成一个个短小的「积木类」——flex 就是 display: flex,p-4 就是 padding: 1rem,text-lg 就是 font-size: 1.125rem。写样式 = 在 className 里搭积木,不用离开 JSX 文件,也不用自己起名。

面试角度:Tailwind 的优劣、和 CSS Modules 的对比、暗色模式怎么实现,都是前端面试的常备题。

先建立图像

一个类名拆解给你看——Tailwind 类名是「属性 + 档位」的编码:

text-gray-600
│    │    └─ 色阶(50 最浅 → 950 最深)
│    └────── 颜色名
└─────────── 属性:文字颜色

md:flex-row
└┬┘└──┬───┘
 前缀   属性:值
 └── 响应式断点:≥768px 才生效

dark:bg-gray-900
└─┬┘
  暗色模式前缀:系统为暗色时生效

常用断点与间距档位(背下来,写响应式全靠它):

前缀生效宽度语义
(无)0px+移动优先的默认样式
sm:≥640px大手机横屏
md:≥768px平板
lg:≥1024px笔记本
xl:≥1280px桌面
类实际值类实际值
p-10.25remp-41rem
p-20.5remp-61.5rem
p-3
0.75rem
p-8
2rem

跟着做

步骤 1:改造默认首页,理解工具类

把 src/app/page.tsx 换成:

export default function Home() {
  return (
    <main className="min-h-screen bg-gray-50 p-6">
      <div className="mx-auto max-w-2xl rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
        <h1 className="text-2xl font-bold text-gray-900">课程卡片</h1>
        <p className="mt-2 text-sm text-gray-600">
          我是一段说明文字,颜色比标题浅一档。
        </p>
        <button className="mt-4 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 active:bg-blue-800">
          开始学习
        </button>
      </div>
    </main>
  );
}

逐行读类名:容器居中(mx-auto max-w-2xl)、圆角边框阴影(rounded-lg border shadow-sm)、按钮三态(hover: 悬停、active: 按下)。保存即可见,无需任何构建命令。

步骤 2:响应式布局——移动优先

export default function TrackGrid({ tracks }: { tracks: string[] }) {
  return (
    <div className="grid grid-cols-1 gap-4 p-6 md:grid-cols-2 lg:grid-cols-3">
      {tracks.map((t) => (
        <div key={t} className="rounded-lg border p-4">
          <h2 className="font-semibold">{t}</h2>
        </div>
      ))}
    </div>
  );
}

读法:默认(手机)1 列 → md 起 2 列 → lg 起 3 列。注意思维顺序是「先写手机的,再加前缀覆盖大屏」,反过来写会漏。

验证方式:浏览器窗口拖拽宽度,或 DevTools 切换设备模拟(Ctrl+Shift+M)。

步骤 3:暗色模式

Tailwind v4(新版 create-next-app 默认)开箱即用 dark: 前缀,跟随系统设置:

<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
  <p>白天白底黑字,夜里黑底白字。</p>
</div>

想手动切换(不跟系统)时,用类名策略——在全局 CSS 声明自定义变体(v4 语法):

/* src/app/globals.css */
@import "tailwindcss";

@custom-variant dark (&:where(.dark, .dark *)); /* dark: 改为认 .dark 类 */
// src/components/ThemeToggle.tsx
"use client";

import { useState } from "react";

export default function ThemeToggle() {
  const [dark, setDark] = useState(false);
  const toggle = () => {
    setDark(!dark);
    document.documentElement.classList.toggle("dark", !dark);
  };
  return (
    <button onClick={toggle} className="rounded border px-3 py-1 text-sm">
      {dark ? "☀️ 白天" : "🌙 夜间"}
    </button>
  );
}

(Tailwind v3 的项目用 darkMode: "class" 配置达到同样效果,写法不同原理一致。)

步骤 4:条件类名用 clsx,不手写字符串拼接

三元拼接字符串是新手重灾区(容易漏空格):

// ❌ 手拼:注意 f Ring 前面那个空格,极易漏
<div className={`border p-3 ${active ? "bg-blue-50" : ""}`}>

// ✅ clsx:数组 + 对象,自动处理空格与 falsy
import clsx from "clsx";

<div className={clsx("border p-3", active && "bg-blue-50", error && "border-red-500")}>
npm install clsx

企业项目常再包一层 cn(合并 + 去重 + 冲突裁决,配 tailwind-merge):

// src/lib/cn.ts
import clsx, { type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

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

好处:cn("p-4", active && "p-6") 会聪明地只保留 p-6(同属性冲突时取后者),手拼字符串做不到。

步骤 5:语义组件类——本站的混合打法

纯工具类有堆积问题:一个按钮的 className 可能 10 个类起步,到处复制。本站的做法是在 globals.css 里定义少量语义类,内部用 Tailwind 的 @apply 思路组合(v4 直接复用工具类定义组件类):

/* src/app/globals.css */
@import "tailwindcss";

@layer components {
  .gh-box {
    @apply rounded-lg border border-gray-200 bg-white shadow-sm;
  }
  .gh-box-header {
    @apply flex items-center gap-2 border-b border-gray-100 px-4 py-3;
  }
}
// 页面里就可以这样用(本站课时页的真实用法)
<div className="gh-box gh-box-header">
  <h1 className="text-lg font-semibold">课时标题</h1>
</div>

这套「高频组合沉淀为语义类 + 低频细节直接写工具类」的分层,是企业级 Tailwind 项目的标准打法:既保留工具类的灵活性,又避免同一组合复制十遍。

讲清楚要点

  1. Next.js 四种样式方案的定位:全局 CSS(globals.css,放基础样式与语义类);CSS Modules(*.module.css,自动哈希类名防冲突,适合独立复杂组件);Tailwind(原子类,本站主力);CSS-in-JS(运行时注入,与 RSC 冲突,本项目排除)。选型一句话:默认 Tailwind,特殊情况 CSS Modules,别引入运行时方案。
  2. 移动优先是纪律不是偏好:无前缀类 = 手机端样式,md: 等前缀 =「此宽度以上覆盖」。所有响应式 bug 几乎都源于把顺序写反。
  3. hover:/dark:/md: 前缀可叠加:dark:hover:bg-gray-700 = 暗色模式且悬停时生效。叠加顺序是「变体前缀从左到右嵌套」,读类名时从右往左读出核心属性。
  4. 类名冲突要靠工具裁决:条件组合场景一律 cn()(clsx + tailwind-merge),手拼字符串的三种死法:漏空格、falsy 值混进类名、同属性类并存行为不可预期。
  5. 样式分层心法:设计规范里的固定组合(卡片、按钮、盒子)沉淀为 @layer components 语义类;页面局部的一次性布局直接写工具类;主题色、间距等设计 token 用 CSS 变量(--color-*)统一管理。本站 DESIGN.md 就是这套规范的文档化。

你可能会懵的地方

  • Tailwind 类名这么长,HTML 不会很乱吗? 习惯了就好,而且换来三个好处:样式与结构同文件(改一处就完)、无命名冲突、无死代码。真正乱的是「样式定义散落 N 个文件、改按钮要全局搜索」。本站配合语义类沉淀后,页面上类名长度是可控的。

  • Tailwind v3 和 v4 差别大吗? 核心工具类完全一样,差别在配置方式:v4 用 CSS 里 @import "tailwindcss" + @custom-variant 等 CSS 优先语法,不再强制 tailwind.config.js;v3 用 JS 配置文件。课程示例按 v4 写,v3 项目查一下对应配置名即可迁移。

  • 什么时候该用 CSS Modules 而不是 Tailwind? 复杂动画、第三方样式库深度定制、需要精确级联控制的独立模块。判断法:这个样式「能不能用有限的工具类表达」——能就 Tailwind,表达不了(如复杂 keyframes、伪元素堆叠)就 module 文件。

  • @apply 为什么被一些人反对? 它把 Tailwind 退化为「起名版的普通 CSS」,丧失原子类的一致性约束;大量 @apply 等于回到传统 CSS 老路。本站只在 components 层沉淀高频组合时用它,页面细节仍直接写工具类——克制的使用没问题。

  • 暗色模式只写 dark: 会漏样式吗? 会。心智是「成对补齐」:每写一个颜色类,立刻想它的 dark 对应物。漏配的表现是夜间模式下白底刺眼。工程上可用 lint 插件或代码评审清单兜底。

错法对照

错法问题正解
className={"p-4" + active && "bg-red-500"}运算优先级灾难,false 会渲染出 "false"cn("p-4", active && "bg-red-500")
大屏样式写前面 lg:flex 后写默认 flex-col覆盖顺序错,断点失效无前缀写手机样式,前缀类覆盖大屏
手写 style={{ fontSize: 16 }} 内联无法复用、无法响应式、难以主题化一律 Tailwind 类,动态值用 CSS 变量
每个 div 重复粘贴 10 个工具类改一处要改十处高频组合沉淀为 @layer components 语义类
引入 styled-components 做新组件与 Server 组件冲突,运行时开销用 Tailwind 或 CSS Modules

卡点急救

  1. 写了类名没反应:检查拼写(Tailwind 类名全部小写连字符,bg-gray 少了色阶数字如 bg-gray-600 不存在);动态拼接的类名如 text-${color}-600 不会被构建器识别(它扫描的是完整字符串)——改用完整类名映射对象。
  2. dark: 不生效:确认 globals.css 有 @import "tailwindcss"(v4);手动模式确认 @custom-variant dark 已声明且 JS 确实切换了 html 的 class。
  3. 样式在 dev 正常、build 后丢失:类名是运行时拼接的(扫描不到字符串)或写在 .gitignore 掉的文件里。把动态类名改成静态映射表,这是 Tailwind 最经典的坑。

面试常问

  • Tailwind 的优缺点?和 CSS Modules 怎么选? 优点:无命名冲突、无死代码(按需生成)、样式与组件同文件、响应式/暗色等变体系统统一、对 RSC 零运行时开销。缺点:类名冗长、有学习曲线、复杂样式(动画、伪元素链)表达力弱。选型:常规 UI 用 Tailwind;复杂动画或深度定制用 CSS Modules;CSS-in-JS 因与 RSC 冲突在新 Next 项目中慎用。

  • Tailwind 怎么实现暗色模式? 两种策略:media(跟随系统,dark: 前缀映射到 prefers-color-scheme 查询)和 class(映射到 html 根元素的 .dark 类,配合 JS 切换与 localStorage 记忆)。v4 里通过 @custom-variant dark 声明策略,v3 里用 darkMode: "class"。产品级方案通常选 class 策略,因为要提供用户手动开关。

  • 为什么你们的 Server 组件项目不用 styled-components? CSS-in-JS 依赖运行时收集样式并注入 style 标签,这套机制工作在浏览器里;Server 组件不在浏览器执行、也不水合,二者冲突(要么组件被迫 Client 化丢掉 RSC 收益,要么样式注入失败)。Tailwind 是构建期生成纯 CSS 的方案,无运行时,天然适配 RSC——这是 RSC 时代样式选型的决定性因素。

自测三问

  1. lg:dark:hover:bg-gray-700 这个类在什么条件下生效?
  2. 为什么 className={text-${size}-600} 这种动态拼接是危险的?
  3. 本站 gh-box 这类语义类和直接写工具类,各自的适用边界是什么?

小结一句话

Tailwind 用原子类 + 变体前缀把样式写进 JSX,移动优先、cn 管条件、语义类沉淀高频组合,是 RSC 时代的默认样式答案。

延伸

  • Tailwind CSS 官方文档
  • 下一课:给平台加门禁——认证与登录门控。
加载本节练习…
← 上一课MDX 与课时渲染下一课 →认证与登录门控