TailwindCSS 配置系統:@theme CSS-first 設定完整解析與 tailwind.config.js 遷移 | TailwindCSS 完整教學
在 TailwindCSS v4 裡,「配置」不再是一個獨立的 JavaScript 檔案,而是直接寫進你的 CSS。核心是
@theme指令——你在裡面定義的每一個設計 Token,會同時變成一個 CSS 變數,並自動生成對應的 utility class。這是 TailwindCSS 完整教學 系列的第四篇,帶你看懂@theme如何運作、extend擴充與覆寫的分界、常見設定項的命名空間規則,以及如何從tailwind.config.js的思維遷移過來。
前言
**配置系統(configuration)**指的是你告訴 TailwindCSS「顏色、間距、字體、斷點這些設計值長什麼樣子」的機制。v3 把這件事集中在專案根目錄的 tailwind.config.js,用一個 JavaScript 物件描述整套設計系統;v4 則把它搬進 CSS,用 @theme 指令直接定義設計 Token——這就是所謂的 CSS-first 配置。
用一個現實世界的類比來理解:把設計系統想成一台音響的控制面板。v3 的做法是把面板收在一個獨立的儀器箱(tailwind.config.js)裡,要調音色得先打開箱子、找到對應的旋鈕、翻譯成 JS 物件的巢狀結構。v4 則把所有旋鈕、滑桿、色票直接鑲在 CSS 這塊面板上:你轉一個旋鈕(定義一個 --color-* 變數),面板上對應的燈號(bg-*、text-* 等 utility)就同步亮起,而且這顆旋鈕的當前值也隨時能被面板上其他元件(任意 CSS 規則的 var())讀到。「設定」與「可用的變數」合而為一,正是 v4 最核心的心智轉變。
本篇你將學到:
@theme的運作原理——為什麼一個設計 Token 能同時是 CSS 變數又能生成 utility,以及它與:root的關鍵差異- 命名空間規則:
--color-*、--spacing-*、--font-*、--breakpoint-*各自生成哪一類 utility - 擴充 vs 覆寫:
@theme extend(追加)與@theme(覆蓋)的分界,以及何時該用哪一個 - 如何用
@config相容舊的tailwind.config.js,以及一套從 JS 配置遷移到@theme的思維轉換
本系列以 TailwindCSS v4 為預設版本,遇到與 v3 有明顯差異之處會特別標註。
核心概念
兩種配置範式:從 JS 物件到 CSS 指令
上一篇《類別偵測機制》我們看懂了掃描器如何決定「哪些 class 會被產生」;本篇的配置系統則決定「這些 class 產生出什麼樣的值」——兩者是理解 Tailwind 運作的一體兩面。要談配置,得先看清 v3 與 v4 兩種範式的根本差異。
v3(JavaScript 配置) vs v4(CSS-first 配置)
══════════════════════════════════════════════════════════════
v3:tailwind.config.js(獨立 JS 檔)
┌─────────────────────────────────┐
│ // CSS 入口 │
│ @tailwind base; │
│ @tailwind components; │
│ @tailwind utilities; │
│ │
│ // tailwind.config.js │
│ module.exports = { │
│ content: [...], │
│ theme: { extend: { │
│ colors: { brand: '#...' } │
│ } }, │
│ } │
└─────────────────────────────────┘
v4:@theme(寫在 CSS 內)
┌─────────────────────────────────┐
│ @import "tailwindcss"; │
│ │
│ @theme { │
│ --color-brand: oklch(...); │
│ } │
└─────────────────────────────────┘
兩者的差異不只是「檔案換了位置」,而是幾個關鍵面向的轉變:
| 面向 | v3(JavaScript) | v4(CSS-first) |
|---|---|---|
| 設定位置 | tailwind.config.js | CSS 檔內的 @theme 指令 |
| 引入方式 | @tailwind base/components/utilities | @import "tailwindcss" |
| 設計 Token | JS 物件(theme / extend) | CSS 變數(custom properties) |
| 內容偵測 | content 陣列(glob) | 自動偵測 + @source |
| 顏色格式 | 預設十六進位(RGB/HSL) | 預設 OKLCH |
| 主題值取用 | theme() 函式、resolveConfig | 直接用 var(--...) |
| 舊配置相容 | — | 可用 @config 載入舊 JS 配置 |
其中最值得記住的一列是「主題值取用」。在 v3,你若想在自訂 CSS 裡引用主題色,得透過 theme('colors.brand') 這種函式呼叫;在 v4,設計 Token 本身就是 CSS 變數,你直接 var(--color-brand) 就能用。「設定即變數」這個雙重身分,是 v4 一切便利的源頭。
順帶一提 @import "tailwindcss" 這一行到底做了什麼。它其實會展開成一組 @layer 宣告與三個子檔案的匯入,這也解釋了 v4 的樣式層順序:
/* @import "tailwindcss" 實際展開的內容(概念) */
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css" layer(utilities);
理解這一點的好處是:你會知道 @theme 定義的 Token 屬於最底層的 theme 層,而 utility 屬於最上層的 utilities 層——這個層級順序決定了誰能覆蓋誰。這也是為什麼 v4 不再需要 v3 那三行 @tailwind base/components/utilities:一行 @import 就把整套框架連同層級順序一起帶進來了。
另一個要留意的差異是顏色格式。v4 的預設調色盤改用 OKLCH 色彩空間,而非 v3 慣用的十六進位。OKLCH 的好處是「感知均勻」——調整亮度或彩度時,人眼感受到的變化更線性,做深淺色階(如 brand-50 到 brand-950)時色階過渡更自然。你當然還是可以在 @theme 裡繼續用 #3b82f6 這種十六進位值,v4 完全接受;但若你要自建一整套色階,OKLCH 會省下很多手動微調的功夫。
@theme 的運作原理:一份定義,兩種產出
@theme 是 v4 配置的入口。它有一個容易被低估、卻極為關鍵的特性:你在裡面定義的每一個變數,都會同時產生兩種東西——一個 :root 上的 CSS 變數,以及一組對應的 utility class。
@theme → CSS 變數 + Utility Class(一份定義,兩種產出)
══════════════════════════════════════════════════════════════
你撰寫的 @theme
┌─────────────────────────────────┐
│ @theme { │
│ --color-brand: oklch(...); │
│ --spacing-18: 4.5rem; │
│ } │
└────────────────┬────────────────┘
│ Tailwind 處理
┌──────────┴──────────┐
▼ ▼
┌──────────────┐ ┌────────────────────────┐
│ 輸出到 :root │ │ 生成 utility class │
│ │ │ │
│ :root { │ │ .bg-brand { │
│ --color- │ │ background-color: │
│ brand:...; │ │ var(--color-brand); │
│ --spacing- │ │ } │
│ 18:4.5rem; │ │ .p-18 { padding:4.5rem }│
│ } │ │ .m-18 { margin:4.5rem } │
└──────────────┘ └────────────────────────┘
可在任意 CSS 在 HTML 的 class 屬性中
規則裡 var() 引用 直接使用
這個「一份定義、兩種產出」的設計解決了 v3 時代一個很煩人的重複問題。在 v3,如果你既想在 HTML 用 bg-brand,又想在某段自訂 CSS 裡引用同一個品牌色,往往得在 config 定義一次、再手動宣告一個 CSS 變數一次,兩處要同步維護。v4 讓你只寫一次 --color-brand,class 與變數就都有了,永遠不會不同步。
還有一個實務上的甜頭:因為 Token 現在是真正的 CSS 變數,它們會進到瀏覽器的執行環境。這代表你的 JavaScript 也能透過 getComputedStyle(document.documentElement).getPropertyValue('--color-brand') 讀到當前主題色——例如要把品牌色傳給一個 canvas 繪圖 API 或某個第三方圖表函式庫時,不必再把色值硬編一份在 JS 裡。設計 Token 從此有了單一事實來源(single source of truth),CSS、HTML class、JavaScript 三邊都指向同一個 @theme 定義。
命名空間規則:前綴決定生成哪類 utility
@theme 怎麼知道要為某個變數生成哪一類 utility?答案是看變數的命名空間前綴。這是整套配置系統最需要背下來的規則,因為它決定了你自訂的 Token 會變成什麼樣的 class。
| CSS 變數前綴 | 生成的 utility | 範例 |
|---|---|---|
--color-* | 顏色相關(bg-、text-、border- 等) | --color-brand → bg-brand |
--spacing-* | 間距(p-、m-、w-、h-、gap- 等) | --spacing-18 → p-18 |
--font-* | 字體族(font-) | --font-display → font-display |
--font-size-* | 字體大小(text-) | --font-size-hero → text-hero |
--font-weight-* | 字重(font-) | --font-weight-heavy → font-heavy |
--breakpoint-* | 響應式前綴 | --breakpoint-xs → xs: 前綴 |
--radius-* | 圓角(rounded-) | --radius-card → rounded-card |
--shadow-* | 陰影(shadow-) | --shadow-card → shadow-card |
--ease-* | 過渡曲線(ease-) | --ease-snappy → ease-snappy |
理解這張表最好的方式,是抓住它的因果方向:不是「我想要一個 bg-brand,所以去某處註冊它」,而是「我定義了一個 --color-brand,於是 bg-brand(以及所有吃顏色的 utility)自動就有了」。你操作的是設計 Token這個源頭,utility 是它的自然衍生物。這也解釋了為什麼命名前綴不能亂取——--color- 這個前綴本身就是你對 Tailwind 的指令,告訴它「這是一個顏色,請生成顏色類的 utility」。
間距系統:單一 --spacing 推導全部
間距是命名空間規則裡一個特別優雅的例子,值得單獨拉出來講。v4 的間距系統由單一的 --spacing 基準變數推導出所有間距 utility,而不是逐一列舉每個值:
@theme {
--spacing: 0.25rem; /* 基準單位 */
}
/* 於是自動推導:
p-1 = 0.25rem、p-2 = 0.5rem、p-4 = 1rem
w-16 = 4rem(16 × 0.25rem)、mt-8 = 2rem(8 × 0.25rem) */
這帶來一個 v3 沒有的好處:任意倍數的間距不需事先設定就能用。在 v3,如果你想用 grid-cols-15 或 w-17 這種非預設的值,得先在 config 的 spacing 裡加上對應的鍵;v4 因為所有間距都是 --spacing 的倍數,w-17、mt-8、gap-13 這類值開箱即用,掃描器掃到就自動算出來。這是「單一基準變數 + 乘法推導」設計換來的彈性。
實作範例
理論看完,我們來看實際的 CSS——先示範 @theme 定義各類 Token,再對照 extend 的差異,最後是 @config 相容與 v3 到 v4 的遷移。
定義設計 Token:一份完整的 @theme
以下是一份涵蓋顏色、間距、字體、斷點的 @theme 範例,展示各命名空間怎麼寫:
/* src/index.css */
@import "tailwindcss";
@theme {
/* 顏色系統(v4 預設用 oklch 色彩空間,感知均勻) */
--color-brand-50: oklch(0.97 0.02 260);
--color-brand-500: oklch(0.60 0.22 260); /* → bg-brand-500、text-brand-500... */
--color-brand-900: oklch(0.25 0.12 260);
--color-surface: oklch(0.99 0 0); /* → bg-surface、text-surface... */
/* 自訂間距(會生成 p-18、m-18、w-18、gap-18... 等所有間距 utility) */
--spacing-18: 4.5rem;
--spacing-128: 32rem;
/* 字體族(→ font-display、font-mono) */
--font-display: "Cal Sans", "Inter", sans-serif;
--font-mono: "JetBrains Mono", monospace;
/* 字體大小(→ text-hero) */
--font-size-hero: 3.5rem;
/* 斷點(→ 新增 xs: 與 3xl: 響應式前綴,v4 斷點預設用 rem) */
--breakpoint-xs: 30rem; /* 480px */
--breakpoint-3xl: 112rem; /* 1792px */
/* 圓角與陰影(→ rounded-card、shadow-card) */
--radius-card: 0.75rem;
--shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.1);
}
寫完之後,你在 HTML 裡就能直接用 bg-brand-500 p-18 font-display text-hero rounded-card xs:flex 3xl:grid-cols-6 這一整排 class,同時在任何自訂 CSS 規則裡用 var(--color-brand-500) 引用同一個值。
擴充 vs 覆寫:@theme extend 的分界
這是新手最容易踩雷的地方。不帶 extend 的 @theme 會覆蓋你所觸及的命名空間,可能把 Tailwind 的預設值一起清掉;@theme extend 則保留預設並追加:
/* ❌ 覆蓋:@theme 會清掉整個命名空間的預設值 */
@theme {
--color-brand: oklch(0.6 0.24 260);
/* 風險:若這樣做會影響 --color-* 命名空間,
預設的 text-red-500、bg-green-600 等可能失效 */
}
/* ✅ 追加:@theme extend 保留所有預設,只新增 brand */
@theme extend {
--color-brand: oklch(0.6 0.24 260);
/* 預設的 text-red-500、bg-blue-600 全部保留,
同時新增 bg-brand、text-brand */
}
日常擴充品牌色、加幾個自訂間距時,幾乎都該用 @theme extend。只有你確實想完全替換某個命名空間時,才用不帶 extend 的 @theme。v4 還提供 initial 關鍵字做更精確的清除:
@theme {
/* 清空整個顏色命名空間,只保留下面自訂的三個 */
--color-*: initial;
--color-white: #fff;
--color-brand: oklch(0.6 0.24 260);
--color-midnight: oklch(0.2 0.08 260);
/* 執行後只剩 bg-white、bg-brand、bg-midnight,不再有 bg-red-500 */
}
v3 差異:v3 的對應概念是
theme: { ... }(覆蓋)與theme: { extend: { ... } }(追加)。v4 把這個「覆蓋 vs 追加」的選擇搬到了@theme與@theme extend的差別上,心智模型是一致的,只是換了語法載體。
@theme 與 :root:一個常見混淆點
前面提過 @theme 的變數會生成 utility,而 :root 的不會。這在實務上是這樣體現的:
@import "tailwindcss";
@theme {
--color-mint-500: oklch(0.72 0.15 165);
/* ✅ 會產生 bg-mint-500、text-mint-500、border-mint-500... */
}
:root {
--header-height: 64px;
/* 只是普通 CSS 變數,不會產生任何 utility;
供自訂 CSS 用,例如 height: calc(100vh - var(--header-height)) */
}
判斷準則很簡單:這個值需不需要當 class 用? 需要就放 @theme;只是內部 CSS 規則要引用的中間變數,放 :root 即可。另外要記得 @theme 內的變數必須是頂層、不可巢狀。
@config:相容舊的 tailwind.config.js
遷移大型專案時,你不一定能一次把整份 tailwind.config.js 翻成 @theme。這時用 @config 指令把舊配置載回來,讓 JS 配置與 CSS 配置並存:
/* src/index.css */
@import "tailwindcss";
/* 載入既有的 v3 JS 配置(遷移期橋接) */
@config "./tailwind.config.js";
/* 同時可以用 @theme extend 在舊配置之上追加新的 v4 Token */
@theme extend {
--color-new-accent: oklch(0.65 0.18 150);
}
@config 是遷移期的橋樑:它讓你先把 Tailwind 升級到 v4(享受 Oxide 引擎與自動內容偵測),同時暫時保留還沒來得及轉換的 JS 配置,之後再逐步把 config 裡的內容搬進 @theme。這也是某些只支援 JS 配置的第三方外掛在 v4 下仍能運作的關鍵。
從 config.js 到 @theme:遷移思維對照
實際遷移時,把 JS 物件的巢狀結構「攤平」成 CSS 變數即可。對照如下:
// Before(v3)tailwind.config.js
module.exports = {
content: ['./src/**/*.{html,jsx,tsx}'],
theme: {
extend: {
colors: { brand: { 500: '#3b82f6' }, surface: '#fafafa' },
spacing: { 18: '4.5rem' },
fontFamily: { display: ['Cal Sans', 'Inter', 'sans-serif'] },
screens: { xs: '480px' },
},
},
plugins: [require('@tailwindcss/typography')],
}
/* After(v4)src/index.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography"; /* require() → @plugin 指令 */
@theme extend {
--color-brand-500: #3b82f6; /* colors.brand.500 → 扁平變數 */
--color-surface: #fafafa;
--spacing-18: 4.5rem; /* spacing.18 → --spacing-18 */
--font-display: "Cal Sans", "Inter", sans-serif; /* fontFamily.display */
--breakpoint-xs: 30rem; /* screens.xs(480px → 30rem) */
}
遷移思維的幾個重點:
- 不需要
content陣列:v4 自動內容偵測,掃描範圍不用手動維護(上一篇《類別偵測機制》已詳述)。 require()外掛改用@plugin指令:直接寫在 CSS 裡。- JS 物件巢狀 → 扁平 CSS 變數:
colors.brand.500變成--color-brand-500,用連字號攤平層級。 - 斷點單位改
rem:v4 斷點預設用rem,480px寫成30rem。
常見錯誤與最佳實踐
最容易踩的坑
坑一(頭號殺手):該用 extend 卻用了純 @theme,把預設值清掉。 你只是想加一個品牌色,結果整個 --color-* 命名空間被覆蓋,bg-gray-100、text-red-500 突然全失效。日常擴充一律用 @theme extend,除非你明確要替換整個命名空間。
坑二:把想當 class 用的值誤放進 :root。 在 :root 定義 --color-brand 只會得到一個 CSS 變數,bg-brand 不會出現。要生成 utility 就得放進 @theme。
坑三:@theme 內變數巢狀或非頂層。 @theme 的變數必須是頂層宣告,不能像一般 CSS 那樣包在選擇器裡。若需要依主題切換值,做法是在 @theme 定義基準 Token,再到 @layer base 裡用 .dark 等選擇器覆蓋 CSS 變數:
@theme {
--color-bg: oklch(0.99 0 0);
--color-fg: oklch(0.15 0 0);
}
@layer base {
.dark {
--color-bg: oklch(0.15 0 0); /* 覆蓋變數值即可切主題 */
--color-fg: oklch(0.95 0 0);
}
}
坑四:以為 v4 完全不能用 JS 配置。 不是的——@config 讓舊的 tailwind.config.js 仍可載入,遷移不必一步到位。
最佳實踐
- 能
extend就extend:保留 Tailwind 完整的預設設計系統,只追加你需要的,避免無意間破壞既有 class。 - 善用「設定即變數」:需要在自訂 CSS 引用主題值時,直接
var(--color-brand),不要再另外宣告一份重複的變數。 - 語義化命名 Token:除了
--color-brand-500這類原始色,可再定義--color-primary: var(--color-brand-500)這種語義層,日後換色只改一處。 - 遷移分階段:先用
@config橋接讓專案跑在 v4 上,再把 config 內容逐塊搬進@theme,最後移除@config。
一句話總結:日常擴充用 @theme extend、想當 class 的值放 @theme、內部變數放 :root、遷移期靠 @config 橋接。
小結
這是 TailwindCSS 完整教學 系列的第四篇。上一篇《類別偵測機制》我們看懂了掃描器如何決定「哪些 class 會被產生」,這一篇則深入配置系統——決定「這些 class 產生出什麼樣的值」。回顧幾個重點:
- v4 是 CSS-first 配置。 設定從 v3 的
tailwind.config.js搬進 CSS 的@theme指令,並用@import "tailwindcss"取代@tailwind三行。 - 一份定義、兩種產出。
@theme裡的每個變數同時是:root上的 CSS 變數,又依命名空間前綴生成對應 utility——這也是它與只產生變數的:root的關鍵差異。 - 命名空間前綴決定 utility 類別:
--color-*→bg-/text-、--spacing-*→p-/m-、--font-*→font-、--breakpoint-*→ 響應式前綴;而--spacing單一基準變數更能推導任意倍數間距。 - 擴充 vs 覆寫:日常一律用
@theme extend保留預設並追加,initial可精確清空;純@theme才是覆蓋。 @config是遷移橋樑:讓舊的 JS 配置與新的@theme並存,遷移思維就是把 JS 巢狀物件攤平成扁平 CSS 變數。
理解了配置系統,你就掌握了 Tailwind「值從哪裡來」的全貌。下一篇 《TailwindCSS 指令與函式》,我們將深入 @import、@theme、@layer、@apply、@source、@plugin、@config 這些指令,以及 theme()、--alpha()、--spacing() 等函式的用法——配置定義了設計 Token,而指令與函式則是你在 CSS 裡真正「動用」這些 Token 的工具,兩者合起來構成 v4 CSS-first 的完整語彙。