設計 Token:用 @theme 打造單一真相源頭 | TailwindCSS 完整教學
上一篇《shadcn/ui》我們看到它換膚的祕密,就在那組語意化的 CSS 變數——
--primary、--background、--radius… 這些其實有個更宏大的名字:設計 Token(Design Tokens)。這一篇要帶你系統性地理解:如何在 TailwindCSS v4 用@theme把 Token 變成「同時是 CSS 變數、又是 utility class」的單一真相源頭,並用 primitive / semantic 分層、--alpha()函式與任意值引用,讓品牌、明暗、多主題都能一鍵切換。
前言
設計 Token(Design Tokens) 是設計系統裡最小、最基礎的建構單位。簡單說,它就是「把一個設計決策存成一個具名的值」——像是「主色是這個藍」、「區塊間距是 80px」、「卡片圓角是 10px」,每一個這樣的決策,都被抽出來、取一個名字、集中放在一個地方管理。這樣一來,整個產品的視覺就不再散落在成百上千個寫死的數值裡,而是收斂到一小組可被引用的 Token。
先用一個生活化的類比建立心智模型。設計 Token 就像一份公司統一的「用字典/色卡本」:設計師、前端、iOS、Android 工程師手上都拿著同一本色卡,上面寫著「Brand Green = 這個色號」。當老闆哪天說「品牌色要改」,你不需要跑遍每個房間去改每一面牆,只要改色卡本裡那一格,所有引用它的地方就自動跟著變。反過來說,如果每個人各自憑印象調色(寫死的 #3B82F6 散落各處),那換色就是一場災難。設計 Token 的核心價值,就是把「同一個設計決策」集中到「單一真相源頭(single source of truth)」。
本系列以 TailwindCSS v4 為預設版本。本篇你將學到:
- Token 分層:
primitive(原始值)與semantic(語義值)兩層如何分工,以及為什麼別過度抽象 @theme的雙重身分:v4 如何讓一個 Token 同時是--var(CSS 變數)又是 utility class- 命名空間(namespace):
--color-*、--spacing-*、--font-*等前綴如何決定生成哪類 utility - v4 專屬語法:
--alpha()、--spacing()函式,以及任意值[...]與簡寫(--var)的引用方式 - 與設計工具對接:從 Figma Variables 到
@theme的工作流思路
本系列以 TailwindCSS v4 為預設版本,範例皆以 v4 語法示範。凡涉及 v3 差異之處,我會特別標註。
核心概念
設計 Token 的分層:primitive 與 semantic
一套成熟的 Token 系統不會把所有值攤平在同一層,而是分層——最常見的是兩到三層。理解分層,是理解整套設計系統的關鍵。
- 第一層:原始 Token(Primitive / Global)。這是「具體的、無語義的」值,例如
--color-blue-600、--spacing-4。它們只描述「是什麼」(這是第 600 號的藍),不描述「用來做什麼」。這一層是你的調色盤與尺標,提供所有可能被選用的原料。 - 第二層:語義 Token(Semantic)。這一層給原始值取一個有意義的別名,例如
--color-primary: var(--color-blue-600)、--color-destructive: var(--color-red-600)。它描述的是「角色」——「主色」「危險色」「頁面背景」——而不是「哪個色號」。元件應該引用這一層(用bg-primary而非bg-blue-600),因為角色是穩定的,而背後綁哪個原始色可以隨時抽換。 - 第三層(選用):元件 Token(Component)。更細的
--color-button-bg: var(--color-primary)這類。多數專案不需要做到這層,過度細化反而是負擔(後面「常見錯誤」會談)。
分層的威力在於換膚只動語義層的指向:當你想把主色從藍換成青,你不必碰任何元件,只要讓 --color-primary 改指向 --color-cyan-600 即可。原始層是原料、語義層是決策、元件層(若有)是消費——這條「primitive → semantic → 使用」的鏈路,就是設計系統的骨架。
Token 分層架構(三層模型)
──────────────────────────────────────────────
[第一層:原始值 Primitive] 「是什麼」
--color-blue-600 --color-red-600 --spacing-4
│
▼ 引用 var()
[第二層:語義值 Semantic] 「什麼角色」
--color-primary: var(--color-blue-600)
--color-destructive: var(--color-red-600)
│
▼ 生成 utility / 被引用
[使用端]
bg-primary → background-color: var(--color-primary)
──────────────────────────────────────────────
換膚只需改「語義層」的指向,元件完全不用動
@theme 的雙重身分:既是 CSS 變數、又是 utility
v4 最核心的轉變,是把設定從 v3 的 JavaScript config(tailwind.config.js)搬進 CSS,並用 @theme 指令定義 Token。而 @theme 裡的每個 Token 都有雙重身分:
- 它是一個 CSS 變數:會原封不動輸出到
:root,例如--color-brand-500,你能在原生 CSS、行內style、JavaScript 裡用var(--color-brand-500)取用。 - 它同時生成 utility class:Tailwind 依它的命名空間自動產生對應的 utility 與 variant,例如
bg-brand-500、text-brand-500、border-brand-500。
換句話說,你只寫一次 Token,就同時得到「給 HTML class 用的 utility」和「給 CSS/JS 用的變數」兩種消費方式,兩邊讀的是同一份值,永不失同步。這就是 v4 「CSS-first」哲學的精髓。下面這張表把 v3 與 v4 的差異、以及命名空間如何決定生成哪類 utility 講清楚:
| 面向 | TailwindCSS v3 | TailwindCSS v4(@theme) |
|---|---|---|
| 設定位置 | tailwind.config.js(JS) | globals.css 裡的 @theme { }(CSS) |
| Token 形式 | JS 物件的鍵值 | CSS custom property(--color-* 等) |
| 能否直接當變數用 | 否(要另外 expose) | 是,自動輸出到 :root |
--color-primary | 生成 bg-primary 等 | 生成 bg-primary 且 可 var(--color-primary) |
| 取值函式 | theme('colors.blue.500') | 直接 var(--color-blue-500) |
| 帶透明度取值 | theme('colors.blue.500 / 50%') | --alpha(var(--color-blue-500) / 50%) |
命名空間(namespace)決定生成哪類 utility
@theme 不是隨便取名都會生效——Token 的前綴命名空間決定了 Tailwind 幫你生成哪一類 utility。這是必須記住的對應關係:
--color-*→ 顏色類:bg-*、text-*、border-*、fill-*、ring-*…--spacing-*→ 間距/尺寸類:p-*、m-*、gap-*、w-*、h-*…--font-*→ 字型家族:font-*--text-*→ 字級:text-*(可附--line-height等子屬性)--radius-*→ 圓角:rounded-*--shadow-*→ 陰影:shadow-*--breakpoint-*→ 響應式斷點:sm:、md:、lg:variant--animate-*→ 動畫:animate-*
所以當你寫 --color-brand: oklch(...),你就自動有了 bg-brand、text-brand;寫 --spacing-section: 5rem,就有了 p-section、gap-section。命名空間是 Token 與 utility 之間的橋樑——記錯前綴(例如把顏色寫成 --brand-color-*),Tailwind 就不會生成你期待的 class。
@theme 的三種模式與關鍵術語
@theme 有三種寫法,各有用途:
@theme { }:擴展預設主題,保留 Tailwind 內建的所有值,只新增你的 Token。最常用。@theme default { }:定義可被後續@theme覆蓋的預設值,適合函式庫作者提供可被使用者改寫的基礎。@theme inline { }:讓 Token 的值直接內嵌進生成的 utility,而非透過var()間接引用。當 Token 的值來自外部變數、且要在切換主題時自動跟隨時(避免 CSS cascade 的解析時機問題),必須用它。
其他關鍵術語:
- primitive / semantic Token:原始值層與語義角色層,前者是原料、後者是決策。
- 命名空間(namespace):
--color-*等前綴,決定生成哪類 utility。 --alpha():v4 調整顏色透明度的函式,編譯為color-mix()。--spacing():v4 依--spacing基準單位換算間距的函式,取代 v3 的theme('spacing.n')。- 任意值
[...]與簡寫(--var):在 utility 中引用任意值或 CSS 變數的兩種寫法。
實作範例
理論看完,我們建一套可執行的 Token 系統:先定 primitive 品牌色,再疊 semantic 角色色,接著示範 --alpha()、--spacing() 與任意值引用。前提是你已有一個 v4 專案(globals.css 裡有 @import "tailwindcss";)。
步驟一:定義 primitive 與 semantic 兩層
在 @theme 裡,先鋪好原始色階(primitive),再用 var() 讓語義色(semantic)指向它。這樣換膚時只需改語義層那一行:
/* globals.css */
@import "tailwindcss";
@theme {
/* === 第一層:原始 Token(Primitive)——調色盤 === */
--color-brand-50: oklch(0.97 0.03 180);
--color-brand-100: oklch(0.94 0.06 180);
--color-brand-500: oklch(0.68 0.15 195); /* teal/cyan 系主色號 */
--color-brand-600: oklch(0.58 0.14 195);
--color-brand-700: oklch(0.48 0.12 195);
/* === 第二層:語義 Token(Semantic)——角色 === */
--color-primary: var(--color-brand-600); /* 主色角色 */
--color-primary-foreground: oklch(1 0 0); /* 主色上的文字 */
--color-destructive: oklch(0.55 0.22 25); /* 危險/刪除 */
--color-background: oklch(1 0 0); /* 頁面背景 */
--color-foreground: oklch(0.15 0.01 195); /* 主要文字 */
--color-border: oklch(0.9 0.01 195); /* 邊框 */
/* === 間距與圓角 Token === */
--spacing-section: 5rem; /* 生成 p-section、gap-section 等 */
--radius-brand: 0.625rem; /* 10px,生成 rounded-brand */
}
定義完成後,你同時擁有兩種用法——這就是「雙重身分」:
<!-- 用法 A:utility class(元件裡最常見) -->
<button class="bg-primary text-primary-foreground rounded-brand px-6">
主要按鈕
</button>
<!-- 用法 B:直接讀 CSS 變數(原生 CSS / 行內 style / JS) -->
<div style="background-color: var(--color-primary)">同一個 Token,用變數取</div>
步驟二:用 semantic 層做明暗與多主題切換
因為元件只引用語義層(bg-primary),換膚時你完全不動元件——只要在不同情境下給語義 Token 不同的值:
/* 深色模式:重新指向語義 Token,元件無感 */
.dark {
--color-background: oklch(0.15 0.01 195);
--color-foreground: oklch(0.97 0.005 195);
--color-primary: var(--color-brand-500); /* 深色下用亮一階的品牌色 */
--color-border: oklch(0.3 0.01 195);
}
/* 多品牌:同一組結構,只換主色指向 */
[data-brand="ocean"] { --color-primary: oklch(0.55 0.15 240); }
[data-brand="forest"] { --color-primary: oklch(0.55 0.15 150); }
<div class="dark"> 或 <div data-brand="ocean"> 包起來,底下所有 bg-primary、text-foreground 就自動換膚——這就是集中管理 Token 的總開關效果。
步驟三:–alpha() 調透明度
在自訂 CSS 裡,v4 用 --alpha() 函式調整顏色透明度,它會編譯成現代的 color-mix():
.glow-card {
/* --alpha():把 brand-500 調成 50% 透明度 */
border-color: --alpha(var(--color-brand-500) / 50%);
/* 編譯結果 → color-mix(in oklab, var(--color-brand-500) 50%, transparent) */
box-shadow: 0 4px 20px --alpha(var(--color-primary) / 30%);
}
如果只是在 HTML 的 utility 裡要透明度,則直接用斜線語法就好,不必寫 --alpha():
<!-- utility 的透明度用斜線,90% 的 primary -->
<button class="bg-primary/90 hover:bg-primary/80">按鈕</button>
步驟四:–spacing() 與任意值引用 CSS 變數
--spacing() 讓你在自訂 CSS 或任意值裡,依 --spacing 基準單位換算間距(取代 v3 的 theme('spacing.4'));而任意值 [...] 與簡寫 (--var) 則讓你把任何 CSS 變數接進 utility:
<!-- 任意值 [var(...)]:引用 Token 變數 -->
<div class="bg-[var(--color-brand-500)]">用完整 var() 語法</div>
<!-- v4 簡寫 (--var):省略 var(),更簡潔 -->
<div class="bg-(--color-brand-500)">簡寫形式</div>
<!-- 簡寫 + 透明度 -->
<div class="bg-(--color-brand-500)/20">20% 透明度的品牌色</div>
<!-- 型別提示:變數型別不明時消歧義 -->
<div class="text-(length:--my-size)">視為字級</div>
<div class="text-(color:--my-color)">視為顏色</div>
<!-- --spacing() 嵌進任意值做混合計算 -->
<div class="py-[calc(--spacing(4)-1px)]">間距減 1px</div>
<!-- Token 是變數,就能和 calc()、light-dark() 組合 -->
<div class="rounded-[calc(var(--radius-brand)-1px)]">圓角減 1px</div>
步驟五:Token 餵給 JavaScript(雙重身分的延伸)
因為 Token 就是 :root 上的 CSS 變數,JavaScript、動畫函式庫能直接讀它,不需要另一套值:
// 讀取計算後的 Token 值(例如給 canvas / 圖表用)
const styles = getComputedStyle(document.documentElement);
const primary = styles.getPropertyValue("--color-primary").trim();
// 動畫函式庫直接吃 Tailwind Token
// <motion.div animate={{ backgroundColor: "var(--color-primary)" }} />
// runtime 動態換主題:改一個變數,全站跟著變
document.documentElement.style.setProperty("--color-primary", "oklch(0.55 0.15 240)");
輸出行為補充:v4 預設「只輸出被用到的 Token」到
:root以精簡體積。若你需要在 JS 讀取全部 Token,可用@theme static { … }強制全部輸出。
常見錯誤與最佳實踐
坑一:過度抽象——做了太多元件級 Token
新手最常見的失誤,是為每個元件的每個狀態都造一個 Token:
/* ❌ 過度細化:維護噩夢 */
@theme {
--color-button-primary-hover-bg: oklch(...);
--color-button-primary-active-bg: oklch(...);
--color-card-header-title-color: oklch(...);
}
這種「元件 Token 爆炸」會讓 Token 表膨脹到沒人看得懂,改一個顏色要在十幾個 Token 間追指向。正確做法是停在語義層就好,讓元件用透明度、狀態變體去衍生變化:
/* ✅ 適度:語義層 + utility 衍生狀態 */
@theme {
--color-primary: var(--color-brand-600);
--color-primary-foreground: oklch(1 0 0);
}
/* hover 用透明度衍生,不需要為 hover 另造 Token */
/* <button class="bg-primary hover:bg-primary/90"> */
坑二:Token 命名不一致
命名東拼西湊,是設計系統崩壞的開端:
/* ❌ 命名不一致:層次與角色都看不出來 */
--primary-color: ...; /* 前綴顛倒,不會生成 utility */
--color-brand: ...;
--btn-bg: ...;
正確做法是固定一套 [命名空間]-[角色]-[變體/狀態] 規範,而且前綴必須用 Tailwind 認得的命名空間(--color-*、--spacing-*…),否則不會生成對應 utility:
/* ✅ 統一規範,前綴正確 */
--color-primary: ...;
--color-primary-foreground: ...;
--color-destructive: ...;
坑三:硬編碼值,繞過 Token
在元件裡寫死 bg-blue-600,等於讓這塊視覺永遠脫離設計系統的掌控——換膚時它不會跟著變。只要是會隨主題/品牌變動的視覺,一律走語義 Token(bg-primary),把「決定顏色」的權力集中到 Token 這一個地方。
坑四:忽略 primitive 與 semantic 的分層,把角色直接綁死色號
把 --color-primary: oklch(0.58 0.14 195) 直接寫死具體值當然能動,但你就失去了「換一個原始色階、整套語義自動跟著換」的彈性。讓 semantic 指向 primitive(--color-primary: var(--color-brand-600)),換膚才能只動一層。
最佳實踐小結
- 分兩層就好:primitive(調色盤)+ semantic(角色),別急著上元件層。
- 元件只用語義 Token:
bg-primary而非bg-blue-600,把總開關集中到 Token。 - 前綴用對命名空間:
--color-*、--spacing-*… 前綴錯了就不生成 utility。 - 狀態用衍生、別造新 Token:hover/active 用
/90透明度或變體,而非--*-hover-bg。 - 善用雙重身分:元件用 utility、JS/CSS 用
var(),兩邊同一份 Token 永不失同步。 - 與設計工具對接:用 Tokens Studio(Figma)+ Style Dictionary 把 Figma Variables 匯出成
@theme的--color-*/--spacing-*,讓設計與程式碼共用同一份真相。
小結
這是 TailwindCSS 完整教學 系列的第三十二篇。上一篇 《shadcn/ui》 我們看到它換膚的祕密藏在一組語意化的 CSS 變數;而這一篇,我們把那組變數的本質——設計 Token——徹底攤開來看。回顧幾個重點:
- Token 分層:
primitive(原始色階、無語義)與semantic(角色別名)分工,換膚只動語義層的指向,元件完全不用改。 @theme雙重身分:v4 把每個 Token 同時變成:root上的 CSS 變數與對應的 utility class,元件用 class、JS/CSS 用var(),永不失同步。- 命名空間:
--color-*、--spacing-*、--font-*等前綴決定生成哪類 utility,是 Token 與 utility 之間的橋樑。 - v4 專屬語法:
--alpha(var(--c) / 50%)調透明度、--spacing(n)換算間距、任意值[var(...)]與簡寫(--var)引用變數,還能加型別提示消歧義。 - 與 Figma 對接:Tokens Studio + Style Dictionary 讓設計工具與
@theme共用同一份 Token,實現真正的單一真相源頭。
掌握了設計 Token,你手上就有了整套設計系統的「總開關」——但 Token 只是原料與規則,如何把它們組織成一套可規模化、可協作、能跨團隊維護的完整體系,還需要更上層的架構思維。下一篇 《設計系統建構》 就要帶你把 Token、元件、文件與版本策略串成一套真正能落地的設計系統(Design System),讓一人專案到大型團隊都能穩健成長。