Next.js 整合:App Router 裝上 Tailwind v4 | TailwindCSS 完整教學

2026/09/08
Next.js 整合:App Router 裝上 Tailwind v4 | TailwindCSS 完整教學

Next.js 整合 是把 TailwindCSS 從練習專案帶進真實全端產品的關鍵一步。在 App Router 架構下,TailwindCSS v4 幾乎零設定就能運作——create-next-app 直接幫你裝好 v4,globals.css 只需一行 @import "tailwindcss",再靠 @tailwindcss/postcss 接上 Next.js 的建置管線。這一篇帶你走完安裝、next/font 字體整合、Server 與 Client Component 下的 class 用法,並拆解 CSS 順序、Turbopack、class 未生效這三個最常見的坑,讓你在正式專案裡把 Tailwind 用得穩又順。

前言

Next.js 是目前最主流的 React 全端框架,而 TailwindCSS 是它最常被搭配的 CSS 方案——兩者幾乎是現代前端專案的預設組合。所謂「Next.js 整合」,講的就是如何把 Tailwind 正確地接進 Next.js 的建置流程,讓你在 App Router(Next.js 13 之後的新路由架構)裡寫 utility class 時,樣式能穩定生成、正確套用,並且和 next/font、Server Components 這些 Next.js 的核心特性和諧共存。

先用一個生活化的類比。把 Next.js 想成一間已經蓋好、水電管線都配好的房子,而 Tailwind 是你要裝進去的中央空調系統。你不需要重新拉整棟樓的線路,只要找到房子預留好的那個接口(在 v4,就是 PostCSS 這條管線),把空調的主機接上去(@tailwindcss/postcss),再打開總開關(globals.css 裡那一行 @import "tailwindcss"),整間房子的每個房間(每個元件)就都能吹到冷氣(套用 utility class)。v4 的美好之處在於——這個接口已經幫你預留得非常標準,create-next-app 甚至連主機都幫你裝好了,你多半只要確認開關有打開就好。

這種「幾乎零設定」的體驗,和 v3 時代形成鮮明對比。在 v3,你得手動建立 tailwind.config.js、填寫 content 掃描路徑、在 CSS 裡寫三行 @tailwind 指令、還要在 PostCSS 設定裡同時掛上 tailwindcssautoprefixer——任何一步漏掉或填錯,樣式就出不來。v4 把這些步驟大幅收斂,再加上 create-next-app 的自動化,對初學者非常友善。但也正因為「太自動」,一旦真的出問題,反而不知道該從哪裡查起——這也是為什麼本篇後半會用一整節,教你一套「class 沒生效」的排查流程。

本系列以 TailwindCSS v4 為預設版本。本篇你將學到:

  • v4 安裝三要素:create-next-app 一鍵安裝、@tailwindcss/postcss 的角色、globals.css@import "tailwindcss",以及為什麼 v4 不再需要 tailwind.config.js
  • 字體整合:用 next/font 載入字體,透過 CSS 變數對接到 Tailwind v4 的 @theme,同時吃到 Next.js 的字體最佳化紅利
  • Server / Client Component:為什麼 Tailwind 的 class 在兩種元件裡用法完全相同,以及真正該注意的動態 class 問題
  • 三大陷阱:CSS 順序(@import 位置與 layer)、Turbopack 相容性、class 未生效的排查流程

本系列以 TailwindCSS v4 為預設版本,範例皆以 v4 語法示範。凡涉及 v3 差異之處,我會特別標註。

核心概念

v4 + Next.js 的整合架構:PostCSS 是那條管線

要理解整合,先看清楚資料怎麼流。在 Next.js App Router 裡,一段 Tailwind class 從你寫下到變成瀏覽器裡的樣式,走的是這條路:你的元件檔(.tsx)裡寫了 className="bg-white p-4" → Next.js 的建置流程掃過這些檔案 → PostCSS 這條管線把 globals.css 交給 @tailwindcss/postcss 這個 plugin 處理 → Tailwind 的 Oxide 引擎掃描專案、只生成用到的 utility → 產出最終的 CSS bundle,注入頁面。

這裡的關鍵角色是 @tailwindcss/postcss。它是 v4 為 PostCSS 環境準備的官方 plugin,扮演「Tailwind 與 Next.js 之間的轉接頭」。Next.js(App Router)預設就走 PostCSS pipeline,所以在 Next.js 裡整合 Tailwind v4,標準答案就是用這個 plugin——你不需要、也不應該去裝 @tailwindcss/vite(那是給純 Vite 專案的,Next.js 用不到)。這是一個很多人會踩的分岔:v4 官方為了效能,在純 Vite 環境推薦效能更好的 @tailwindcss/vite,於是有人照抄到 Next.js 專案,結果接不上——因為 Next.js 沒有把 Tailwind 掛在 Vite,而是掛在 PostCSS 這條線上。記住這條分界:Next.js → @tailwindcss/postcss,純 Vite → @tailwindcss/vite

值得留意的是,這條 PostCSS 管線是「隱形」的——你不會在程式碼裡直接呼叫它,而是 Next.js 在建置時自動去讀 postcss.config.mjs,把裡面列的 plugin 依序套用到你所有的 CSS。所以整合 Tailwind 的動作,本質上就是「在這份 PostCSS 設定裡登記 @tailwindcss/postcss 這個 plugin」,剩下的掃描、生成、注入,Next.js 和 Tailwind 會自己接力完成。這也解釋了為什麼 v4 的安裝步驟這麼少:你要做的只是把轉接頭接上,而不是自己搭整條管線。

項目v3v4(本篇預設)
設定檔tailwind.config.js(必要)可選,設定改寫進 CSS 的 @theme
CSS 入口@tailwind base/components/utilities 三行@import "tailwindcss" 一行
PostCSS plugintailwindcss + autoprefixer@tailwindcss/postcss(內建 prefix)
Content 掃描需在 config 手動列 content自動偵測
JIT需開啟預設(Oxide 引擎)

這張表的每一列都是升級 v4 時的一個「行為轉向」,也對應到後面〈常見錯誤〉會講的坑。現在只要記住一句話:v4 把設定重心從 JS 搬到了 CSS——CSS 入口變成單行 @import、設定 token 寫進 @theme、掃描交給引擎自動處理,tailwind.config.js 從必需品變成了選配。

為什麼 v4 不再需要 tailwind.config.js

這是很多從 v3 過來的人最不習慣的一點。在 v3,tailwind.config.js 是專案的中樞——content 路徑、主題色、字體、外掛全寫在這裡。v4 則採取「CSS-first 設定」:預設不再自動讀取 tailwind.config.js,取而代之的是把設計 token 直接寫在 CSS 的 @theme 區塊裡。

這樣做有兩個好處:設定和樣式放在同一個地方(CSS),心智負擔更低;而且 Tailwind 靠 Oxide 自動偵測掃描範圍,content 陣列也不用手動維護了。對 Next.js 專案來說這尤其省事——過去在 v3 你得小心翼翼地把 app/components/pages/ 等目錄全列進 content 陣列,漏一個目錄那裡的樣式就會神秘消失;v4 直接自動掃描、還會尊重 .gitignore 略過 node_modules 與建置產物,這類「忘了加路徑」的坑就此消失。

如果你有既有的 v3 專案,設定又太複雜、一時搬不動,v4 仍留了一條退路——用 @config 指令明確引入舊的 JS 設定檔,讓你漸進遷移,不必一次改完:

@import "tailwindcss";
@config "../../tailwind.config.js"; /* 明確沿用舊的 v3 設定,漸進遷移 */

Server Components 與 Client Components 下的 class

App Router 引入了 React Server Components(RSC):元件預設在伺服器端渲染,只有在檔案頂部標上 "use client" 的元件才會變成 Client Component(帶到瀏覽器、可用狀態與互動)。這個分野對 Tailwind 有沒有影響?答案是——幾乎沒有,而這正是 Tailwind 在 Next.js 裡好用的原因之一。

原因在於 Tailwind 的 utility class 是編譯期產生的靜態 CSS,它跟「這個元件在伺服器跑還是在瀏覽器跑」完全脫鉤。你在 Server Component 裡寫 className="flex gap-3",和在 Client Component 裡寫,語法、效果、產出的 CSS 都一模一樣。差別只在於元件本身的執行模型:Server Component 的樣式在伺服器端就套用完成、不帶 runtime JS;Client Component 因為要處理互動才存在,但它「怎麼用 Tailwind」沒有任何不同。

所以本篇要傳達的重點是:別把 Server/Client 的分野和 Tailwind 用法綁在一起想。你真正要小心的,不是元件類型,而是「動態拼接 class」——這在兩種元件裡都一樣是地雷,後面〈常見錯誤〉會專門講。順帶一提,這也是 Tailwind 之於 App Router 的一大優勢:很多傳統 CSS-in-JS 方案(把樣式邏輯寫在 JS 執行期)在 Server Components 世界裡會遇到「元件在伺服器端沒有瀏覽器環境可跑」的尷尬,得額外處理;而 Tailwind 因為樣式在建置期就變成純 CSS,天生就和 RSC 相容,不需要為了搭 App Router 而改寫任何寫法。

Tailwind 與 CSS Modules 在 Next.js 裡如何共存

Next.js 原生支援 CSS Modules(.module.css),很多人會問:用了 Tailwind 還需要它嗎?答案是——兩者可以共存,各司其職,不是二選一。原則很簡單:絕大多數的版面、間距、顏色、排版、響應式,交給 Tailwind 的 utility class,寫起來快、不用命名、不用切檔案;而少數 utility 難以表達的情境,才交給 CSS Modules

哪些情境該用 CSS Modules?典型有四類:多個 keyframe 的複雜動畫、需要複雜 ::before / ::after 生成內容的效果、覆蓋第三方套件深層樣式、以及用 utility 寫起來會非常冗長的複雜選擇器。這些情況用一個 scoped 的 .module.css 反而更乾淨。重點在於心態:不是「Tailwind 派 vs CSS Modules 派」的站隊,而是「哪個工具處理這個問題最省事就用哪個」。實務上你甚至可以在同一個元件裡,用 Tailwind class 處理布局、同時掛一個 module class 處理那段複雜動畫,兩者互不干擾。

實作範例

理論說完,實際把 Tailwind v4 裝進一個 Next.js App Router 專案,從安裝到字體整合到元件用法,走一遍完整流程。

範例一:安裝——一鍵腳手架與手動安裝兩條路

最省事的是用官方腳手架。建立新專案時勾選 Tailwind,create-next-app 會把 v4 版本的 Tailwind、@tailwindcss/postcsspostcss.config.mjs、以及一支已寫好 @importglobals.css 全部裝好:

# 建立新的 Next.js 專案,並選用 TailwindCSS(v4 開箱即用)
npx create-next-app@latest my-app --tailwind

如果是要在既有 Next.js 專案裡加上 Tailwind v4,就手動裝三步。第一步,安裝依賴:

# 安裝 Tailwind v4 與 Next.js(PostCSS 管線)所需的 plugin
npm install tailwindcss @tailwindcss/postcss postcss

第二步,在專案根目錄建立(或修改)PostCSS 設定,把 @tailwindcss/postcss 掛上去。注意 plugin 名稱是 @tailwindcss/postcss,不是 v3 的 tailwindcss:

// postcss.config.mjs
const config = {
  plugins: {
    "@tailwindcss/postcss": {},
    // v4 內建 vendor prefix,通常不再需要 autoprefixer
  },
};

export default config;

第三步,在你的全域 CSS 入口寫上那一行 @import,這是 v4 唯一必要的一行:

/* app/globals.css — v4 只需要這一行,不需要 tailwind.config.js */
@import "tailwindcss";

最後,務必確認 root layout 有把這支 CSS 載進來——這一步漏了,Tailwind 樣式就完全不會出現:

// app/layout.tsx — globals.css 必須被 root layout 引入
import "./globals.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="zh-TW">
      <body>{children}</body>
    </html>
  );
}

範例二:用 next/font 整合字體,對接 @theme

Next.js 的 next/font 會自動最佳化字體(自架、消除版面位移、自動 preload),它和 Tailwind v4 的 @theme 可以透過 CSS 變數無縫接起來。做法是:在 layout 載入字體並指定 variable,把變數掛到 <html> 上,再於 @theme 裡把這些變數對應成 Tailwind 的字體 token。

先在 root layout 載入字體。這裡示範同時載入英文 Inter 與繁體中文 Noto Sans TC,各自產生一個 CSS 變數:

// app/layout.tsx
import { Inter, Noto_Sans_TC } from "next/font/google";
import "./globals.css";

const inter = Inter({
  subsets: ["latin"],
  variable: "--font-inter", // 產生一個 CSS 變數供 Tailwind 對接
});

const notoSansTC = Noto_Sans_TC({
  subsets: ["chinese-traditional"],
  variable: "--font-noto-tc",
});

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    // 把字體變數掛到 html,讓整棵樹都能存取
    <html lang="zh-TW" className={`${inter.variable} ${notoSansTC.variable}`}>
      <body className="font-sans antialiased">{children}</body>
    </html>
  );
}

接著在 globals.css@theme 裡,把 next/font 產生的變數對應到 Tailwind 的字體 token。這樣一來 font-sansfont-tc 這些 utility class 就會指向你載入的字體:

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

@theme {
  /* 把 next/font 的 CSS 變數對應成 Tailwind 字體 token */
  --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
  --font-tc: var(--font-noto-tc), "Noto Sans TC", sans-serif;
}

之後在任何元件裡,就能直接用對應的 utility class 切換字體:

<!-- font-sans 走 Inter,font-tc 走 Noto Sans TC -->
<h1 class="font-sans text-3xl font-bold">Inter Heading</h1>
<p class="font-tc text-base leading-relaxed">這是繁體中文段落。</p>

範例三:Server Component 與 Client Component 的實際寫法

如前所述,兩種元件寫 Tailwind 的方式完全相同。先看一個 Server Component(App Router 預設,不用寫任何指令)——它在伺服器端就完成樣式套用,不帶 runtime JS:

// app/components/ProductCard.tsx — Server Component(預設)
// Tailwind 在 Server Component 中完全正常運作
export function ProductCard({
  name,
  price,
}: {
  name: string;
  price: number;
}) {
  return (
    <div className="bg-white rounded-xl shadow-sm border border-gray-200 p-4 hover:shadow-md transition-shadow">
      <h3 className="font-semibold text-gray-900 mb-1">{name}</h3>
      <p className="text-cyan-600 font-bold">NT${price}</p>
    </div>
  );
}

再看一個 Client Component——因為要用 useState 處理互動,檔案頂部標上 "use client"。注意它寫 Tailwind 的方式和上面沒有任何差別:

// app/components/Counter.tsx — Client Component(需要狀態/互動)
"use client";
import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div className="flex items-center gap-3">
      <button
        onClick={() => setCount((c) => c - 1)}
        className="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center"
      >
        
      </button>
      <span className="text-lg font-semibold w-8 text-center">{count}</span>
      <button
        onClick={() => setCount((c) => c + 1)}
        className="w-8 h-8 rounded-full bg-cyan-600 hover:bg-cyan-700 text-white flex items-center justify-center"
      >
        +
      </button>
    </div>
  );
}

範例四:用 cn() 安全地管理動態 class

實務上元件常需要依 props 切換樣式(不同 variant、size)。這時最穩的做法是用 clsxtailwind-merge 封裝一個 cn() 工具——它能用物件語法選擇完整 class 字串(讓 Tailwind 掃得到),還能自動解決 class 衝突:

npm install clsx tailwind-merge
// lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

// cn() 合併 class 並自動解決 Tailwind 衝突
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

使用時,用物件語法把「條件 → 完整 class 字串」對應起來,每個 class 都是靜態可掃描的完整字串:

// components/Button.tsx
import { cn } from "@/lib/utils";

function Button({
  variant = "primary",
  className,
}: {
  variant?: "primary" | "secondary";
  className?: string;
}) {
  return (
    <button
      className={cn(
        "inline-flex items-center justify-center rounded-lg px-4 py-2 font-medium transition-colors",
        {
          "bg-cyan-600 text-white hover:bg-cyan-700": variant === "primary",
          "bg-gray-100 text-gray-900 hover:bg-gray-200": variant === "secondary",
        },
        className // 允許外部覆蓋,twMerge 會處理衝突
      )}
    />
  );
}

常見錯誤與最佳實踐

坑一:CSS 順序——@import 位置與 layer 疊放

CSS 有「後者覆蓋前者」的特性,順序錯了樣式就會被意外蓋掉。在 Next.js + Tailwind v4 有兩個順序要點。

第一,@import "tailwindcss" 要放在 globals.css 的最頂部。 CSS 規範要求所有 @import 必須在其他規則之前;若你在 @import 上面先寫了自訂樣式,@import 可能失效,Tailwind 就整包沒載入:

/* ❌ 錯誤:自訂樣式寫在 @import 之上,@import 可能失效 */
body { margin: 0; }
@import "tailwindcss";

/* ✅ 正確:@import 永遠放最頂部,自訂樣式寫在其後 */
@import "tailwindcss";
body { margin: 0; }

第二,善用 v4 的 layer 而不是硬提高權重。 當你的自訂 CSS 和 utility 打架,別急著加 !important 或堆疊選擇器,那只會讓覆蓋規則越滾越亂。v4 提供 @layer 讓你把自訂樣式明確歸進正確的層級(如 basecomponents),交由既定的 layer 順序決定優先權,行為可預測得多:

@import "tailwindcss";

/* ✅ 用 @layer 把基礎樣式歸進 base 層,順序清楚不打架 */
@layer base {
  h1 { @apply text-3xl font-bold; }
}

坑二:Turbopack 相容性

Next.js 新版預設用 Turbopack(Rust 寫的 bundler)跑 dev。它和 Tailwind v4 的搭配大體良好——基本 utility、@theme 自訂 token、極速 HMR 都支援。但有兩點要放在心上。

其一,部分較特殊的 PostCSS 外掛或複雜的 @plugin 設定,在 Turbopack 下可能相容性未臻完美;若你遇到怪異的建置行為,先試著暫時退回 webpack 比對,能快速判斷問題是否出在 Turbopack。其二,dev 與 build 用的引擎可能不同,養成「上線前用正式 build 指令實際跑一次」的習慣,別只在 dev 模式驗過就當沒事:

{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build"
  }
}

實務建議:開發享受 Turbopack 的極速 HMR,但每次要部署前,務必跑一次正式 build 並在產物上肉眼確認樣式無誤。 大多數專案不會踩到相容性問題,但這道驗證能攔下少數邊界情況。

坑三:class 未生效——三步排查法

「我明明寫了 class,樣式卻沒出來」是 Next.js + Tailwind 最高頻的求救。升級 v4 後尤其常見。照下面順序查,九成能解決:

第一步,檢查 globals.css 頂部的指令。 這是升級 v4 後最常見的疏漏——舊的三行 @tailwind 指令 v4 已不辨識,必須換成單行 @import:

/* ❌ v3 舊寫法,在 v4 不生效,會導致 utility 完全不生成 */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* ✅ v4 正確寫法 */
@import "tailwindcss";

第二步,檢查 PostCSS plugin 名稱。 v4 的 plugin 叫 @tailwindcss/postcss;若還寫著 v3 的 tailwindcss,Tailwind 根本沒被掛進管線:

// ❌ v3 舊名稱,v4 無效
const config = { plugins: { tailwindcss: {}, autoprefixer: {} } };

// ✅ v4 正確名稱
const config = { plugins: { "@tailwindcss/postcss": {} } };

第三步,若只有「某些 class」沒生效,幾乎都是動態拼接。 Tailwind 只做靜態掃描,任何用字串串接組出來的 class 它都看不到。這在 Server 或 Client Component 裡都一樣:

// ❌ 動態拼接:Tailwind 掃不到,bg-cyan-500 不會被生成
const color = "cyan";
<div className={`bg-${color}-500`} />

// ✅ 完整字串對照表:每個 class 都是靜態可掃描的完整字串
const bgMap = { cyan: "bg-cyan-500", red: "bg-red-500" } as const;
<div className={bgMap[color]} />

若上述三步都對,還有兩個補充點:確認 app/layout.tsx 真的有 import "./globals.css"(漏了則 CSS 完全沒載入);以及若你原本重度依賴 tailwind.config.js,記得 v4 預設不再自動讀取它,要嘛把設定搬進 @theme,要嘛用 @config 明確引入舊檔。

最佳實踐小結

  • 安裝走官方路徑:新專案用 create-next-app --tailwind 一鍵到位;既有專案手動裝 @tailwindcss/postcss,別誤用 Vite plugin。
  • CSS 入口記牢一行:@import "tailwindcss"globals.css 最頂部,並確認 root layout 有引入它。
  • 字體用 next/font 對接 @theme:透過 CSS 變數把字體掛進 @theme,同時吃到 Next.js 的字體最佳化。
  • Server/Client 用法一致:別把元件類型和 Tailwind 用法綁在一起想,真正該防的是動態拼接 class。
  • 上線前跑正式 build:享受 Turbopack 的 dev 速度,但部署前務必用 next build 在產物上驗過樣式。
  • class 沒生效照三步查:先看 @import、再看 plugin 名稱、最後查動態拼接,九成問題到此為止。

小結

這是 TailwindCSS 完整教學 系列的第三十九篇。上一篇《效能最佳化》,我們把 Tailwind 的效能拆成建構速度、CSS 產出大小、執行期渲染三個面向,學會用 @source 精準掃描、量測產出並避開 safelist 膨脹;這一篇,我們把調校好的 Tailwind 實際接進當今最主流的 React 框架 Next.js,走完從安裝到上線的完整整合。回顧幾個重點:

  • 整合架構:Next.js App Router 走 PostCSS 管線,v4 靠 @tailwindcss/postcss 這個轉接頭接上,CSS 入口收斂成單行 @import "tailwindcss"
  • v4 CSS-first:設定重心從 tailwind.config.js 搬進 CSS 的 @theme,多數專案零額外設定;需沿用舊設定則用 @config 引入。
  • 字體整合:next/font 產生 CSS 變數,對接 @theme 的字體 token,font-sansfont-tc 即可切換,並自動享有字體最佳化。
  • Server/Client 一致:Tailwind 是編譯期靜態 CSS,兩種元件用法相同,該防的是動態拼接 class。
  • 三大坑:CSS 順序(@import 位置與 layer)、Turbopack(上線前跑正式 build)、class 未生效(三步排查),對照就能穩定避開。

把 Tailwind 接進 Next.js,你就有了打造真實全端產品的完整基礎。下一篇《React 整合》,我們把框架的外殼再剝掉一層,回到 React 本身——看 TailwindCSS 如何搭配 Vite + React、元件庫設計與各種 React 生態工具,讓你不論用哪種 React 專案結構,都能把 Tailwind 用得得心應手。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →