TailwindCSS 指令與函式全解析:@import、@layer、@apply、@utility 與 theme()/--alpha() 完整教學 | TailwindCSS 完整教學

2026/08/05
TailwindCSS 指令與函式全解析:@import、@layer、@apply、@utility 與 theme()/--alpha() 完整教學 | TailwindCSS 完整教學

TailwindCSS v4 裡,指令(directives)與函式(functions)是你在 CSS 檔案裡「動用」設計系統的工具箱。從一行 @import "tailwindcss" 載入整套框架,到用 @layer 控制優先級、@apply 提取 utility、@utility 定義自訂 utility、@custom-variant 造出新變體,再到 theme()--alpha()--spacing() 這些函式取用 Token——這篇是 TailwindCSS 完整教學 系列的第五篇,帶你一次看懂 v4 的指令與函式語彙,並標註它與 v3 的關鍵差異。

前言

**指令(directive)**指的是那些以 @ 開頭、寫在 CSS 檔案裡、由 TailwindCSS 在編譯階段解讀的特殊規則;**函式(function)**則是可以寫在 CSS 屬性值裡、用來取用或計算設計 Token 的小工具。上一篇《配置系統》我們用 @theme 定義了設計 Token——顏色、間距、字體、斷點;而這一篇的指令與函式,就是你真正「拿這些 Token 來做事」的那套動詞。

用一個現實世界的類比來理解:如果把 TailwindCSS 想成一間裝備齊全的廚房,那麼《配置系統》裡的 @theme 就是你事先備好的食材與調味料(設計 Token),而本篇的指令與函式則是廚房裡的各種器具與手法@import "tailwindcss" 是把整套廚房電源打開的總開關;@layer 決定「哪道菜先上、哪道菜後上」的出菜順序(優先級);@apply 是把幾樣調味料預先調成一罐醬汁;@utility 是你自製一件專屬料理工具;而 theme()--alpha() 這些函式,就是你臨場舀一匙食材、稀釋一下濃度的動作。器具用對地方,一頓飯行雲流水;用錯地方,再好的食材也做不出好菜。

本篇你將學到:

  • 入口與層級@import "tailwindcss"(取代 v3 的三行 @tailwind)到底展開了什麼,以及 @layer 如何搭配原生 Cascade Layers 控制優先級
  • 提取與自訂@apply 的正確用法與濫用陷阱,以及 v4 新增的 @utility——為什麼它比 v3 的 @layer utilities 更好用
  • 變體指令@variant(套用既有變體到 CSS)與 @custom-variant(定義新變體前綴)的分工
  • 三個函式theme()--alpha()--spacing() 各自的用途,以及 v4 為何更偏好直接用 var(--...)

本系列以 TailwindCSS v4 為預設版本,遇到與 v3 有明顯差異之處會特別標註。

核心概念

指令與函式全覽:一張對照表

在深入每個指令之前,先建立一張全景地圖,你會更清楚各個工具落在整套流程的哪個位置。v4 的指令大致可分成三類:入口與層級提取與自訂變體與相容;函式則是穿插在 CSS 值裡的小工具。

名稱類型用途v3 / v4
@import "tailwindcss"指令單一入口,載入整套框架與層級v4(取代 v3 的三行 @tailwind
@layer指令把規則放進指定 Cascade Layer,控制優先級兩者皆有(v4 用原生層)
@apply指令把一組 utility 展開成 CSS 規則兩者皆有(語法不變)
@utility指令定義自訂 utility,自動支援所有變體v4 新增
@variant指令在自訂 CSS 中套用既有變體條件v4 新增
@custom-variant指令定義新的變體前綴v4 新增(取代 addVariant()
@reference指令無副作用匯入,讓元件 CSS 能用 @applyv4 新增
@source指令手動指定掃描來源與 safelistv4 新增
theme()函式在 CSS 值裡引用主題 Token兩者皆有(v4 語法微調)
--alpha()函式調整顏色不透明度v4 新增
--spacing()函式--spacing 基準推導間距值v4 新增

這張表裡最需要先記住的一件事是:v4 大量新增了「在 CSS 裡就能做完」的指令@utility@custom-variant@variant),這些在 v3 都得寫 JavaScript 外掛(addUtilities()addVariant())才能辦到。這正是 v4「CSS-first」哲學的延伸——上一篇讓配置搬進 CSS,這一篇讓擴充能力也搬進 CSS。

理解這套語彙時,有個心智框架很有幫助:把指令想成「編譯前」與「編譯後」兩種角色。@import@theme@utility@custom-variant 這些是告訴 Tailwind 該產生什麼的指令,它們在建構階段被消化掉,最終產物是一堆標準 CSS;而 @layer 產生的是真正保留在輸出裡的原生 CSS Cascade Layers。函式那一組也類似:--spacing()--alpha()theme() 在你寫的當下看起來像函式呼叫,但除了 var() 之外,多數會在編譯期就被算成固定值。分清楚「哪些是給 Tailwind 看的指示、哪些會原封不動留到瀏覽器」,你在除錯時就不會對著 DevTools 找一個早已被展開消失的 @apply 發呆。

@import 與四個 Cascade Layer

v4 最顯眼的改變,就是把 v3 那三行 @tailwind 收斂成一行 @import

/* ❌ v3 寫法(v4 已棄用) */
@tailwind base;
@tailwind components;
@tailwind utilities;

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

別小看這行 @import,它其實是一組展開的簡寫。概念上它等同於下面這段——先宣告四個層的順序,再把三個子檔案分別匯入對應的層:

/* @import "tailwindcss" 實際展開(概念) */
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);         /* 預設 Token(CSS 變數)*/
@import "tailwindcss/preflight.css" layer(base);      /* Preflight(CSS 重置)*/
@import "tailwindcss/utilities.css" layer(utilities); /* 所有偵測到的 utility */

這四個層——themebasecomponentsutilities——遵循瀏覽器原生的 CSS Cascade Layers 規範,優先級固定是 theme < base < components < utilities,越後面的層永遠覆蓋越前面的層,而且與選擇器特異性(specificity)無關。這一點是理解整個優先級系統的鑰匙:在同一個層裡才比特異性,跨層時層級順序說了算。

現代瀏覽器對 Cascade Layers 的支援很完整(Chrome 99+、Safari 15.4+、Firefox 97+,都在 2022 年初就到位),所以 v4 直接把它當地基。理解「一行 @import 帶進四個層」之後,你才會懂為什麼 @theme 定義的 Token 屬於最底層、utility 屬於最頂層——這也是配置系統與指令系統銜接的地方。

這裡順帶澄清一個很多人搞不清楚的問題:v4 為什麼要放棄 v3 那看起來更「明確」的三行寫法?表面上三行 @tailwind 各自注入 base、components、utilities,讀起來一目了然;但它其實有兩個缺點。第一,這三行只是 Tailwind 自訂的佔位指令,並非真正的 CSS,瀏覽器與一般 CSS 工具鏈看不懂;第二,它沒有把「層級順序」這個資訊本身表達出來,優先級是 Tailwind 內部硬編的規則。v4 改用原生 @import@layer 之後,整套機制變成標準 CSS——你甚至可以在瀏覽器 DevTools 裡直接看到 @layer 的層級順序,除錯時心裡有底。換句話說,這不只是「少寫兩行」,而是把優先級系統從 Tailwind 的私有規則,換成瀏覽器都認得的公開規範。

@layer:用層級而非特異性控制優先級

@layer 讓你把自訂 CSS 規則放進指定的 cascade layer,藉此決定它的優先級。日常最常用的是 basecomponents 兩層:

@layer base {
  /* HTML 元素的基礎樣式 */
  h1 { @apply text-2xl font-bold; }
  a  { @apply text-blue-600 underline; }
}

@layer components {
  /* 可重用元件:優先級高於 base,低於 utilities */
  .card {
    @apply rounded-lg border border-gray-200 p-6 shadow-sm;
  }
}

為什麼一定要把自訂樣式包進 @layer?因為沒有放進任何 layer 的普通 CSS,優先級比所有具名 layer 都高——這是 CSS 規範的行為。如果你把 .card 直接寫在 @layer 之外,它會蓋過 HTML 裡用的 utility(例如你在某個卡片加了 rounded-none 卻蓋不掉),造成難以預測的覆蓋問題。把它放進 @layer components,utilities 層就能正常覆蓋它,優先級行為才符合直覺。

一個能徹底說明「層級 > 特異性」的例子:

@layer base {
  #hero.banner { color: red; }   /* 特異性很高 */
}
@layer utilities {
  .text-blue-500 { color: blue; } /* 特異性很低,但層級更高 */
}
/* 若 HTML 是 <p id="hero" class="banner text-blue-500">
   最終結果是 color: blue —— utilities 層贏,跟特異性無關 */

實作範例

理論看完,我們來看實際的 CSS。以下依序示範 @apply 提取按鈕元件、@utility 自訂 utility、@custom-variant 造變體,以及三個函式的用法。

@apply:把一組 utility 提取成 CSS 類別

@apply 會把一串 utility 展開成對應的 CSS 規則,最典型的用途是為需要固定 class 名稱的場景(第三方 HTML、設計系統)建立元件樣式:

/* src/components.css */
@reference "./index.css";   /* 讓此檔案能用 @apply,見後續說明 */

@layer components {
  /* 基礎按鈕 */
  .btn {
    @apply inline-flex items-center justify-center gap-2;
    @apply rounded-md px-4 py-2 text-sm font-semibold;
    @apply transition-colors duration-150 ease-in-out;
    @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
    @apply disabled:pointer-events-none disabled:opacity-50;
  }

  /* 主要按鈕:可以引用基礎的 .btn,再疊加自己的樣式 */
  .btn-primary {
    @apply btn bg-blue-600 text-white;
    @apply hover:bg-blue-700 focus-visible:ring-blue-500;
  }

  /* 次要按鈕 */
  .btn-secondary {
    @apply btn border border-gray-200 bg-white text-gray-900;
    @apply hover:bg-gray-50 focus-visible:ring-gray-400;
  }
}

寫完後,HTML 就能用 <button class="btn-primary">儲存</button> 這種語義化 class。要注意 @apply 的語法在 v3 與 v4 是一致的,這是少數沒變的指令。這也讓 @apply 成為 v3 遷移到 v4 時最不需要改動的部分——你既有的 @apply 規則幾乎可以原封不動搬過來。

不過範例裡有個小細節值得注意:.btn-primary 裡我們寫 @apply btn ...,也就是讓一個 class 透過 @apply 引用另一個已定義的 class。這是合法的,前提是被引用的 .btn 本身只由 utility 組成、不會反過來引用 .btn-primary。這種「基礎 class 純 utility、衍生 class 疊加」的分層寫法,是用 @apply 建元件時最穩妥的組織方式,也能避開後面會提到的循環引用陷阱。

值得單獨提的是範例第二行的 @reference。當你的 @apply 不是寫在主 CSS 入口,而是寫在某個元件自己的 CSS 檔(Vue SFC 的 <style>、CSS Module、Svelte 的 <style>)裡時,那個檔案並不知道你的主題定義,@apply btn-primary 會失敗。@reference "./index.css" 就是告訴它「去參考主入口的配置,但不要重複輸出一份 CSS」——它只借用定義、不產生副本,避免同一份樣式被打包很多次。

@utility:v4 定義自訂 utility 的正確方式

當你需要 Tailwind 沒內建的 utility(例如 content-visibility),v4 的做法是 @utility,而不是 v3 的「@layer utilities + 手寫 class」。兩者最大的差別在變體支援

/* ❌ v3 寫法:不會自動支援變體 */
@layer utilities {
  .content-auto { content-visibility: auto; }
}

/* ✅ v4 寫法:自動支援 hover:、lg:、dark: 等所有變體 */
@utility content-auto {
  content-visibility: auto;
}

@utility 定義後,你可以直接在 HTML 疊加任何變體,完全不用額外設定:

<div class="content-auto hover:content-auto lg:content-auto"></div>

@utility 也支援巢狀選擇器,很適合處理 ::-webkit-scrollbar 這類偽元素:

@utility scrollbar-hidden {
  &::-webkit-scrollbar { display: none; }
}

這個「自動支援變體」的差異看似小,實際影響很大。想像你在 v3 用 @layer utilities 手寫了一個 .content-auto,某天設計要求「這個效果只在大螢幕才套用」,你會發現 lg:content-auto 根本不生效,得回頭手動為每個斷點各寫一份,維護成本瞬間爆炸。v4 的 @utility 從根本解決了這件事——它把你的自訂 utility 當成 Tailwind 內建 utility 的一等公民,所有變體引擎自動接管。這正是「用對指令」與「用錯指令」在長期維護上的差距。

進階一點,@utility 還能做函式型 utility——用 --value() 接收參數,同時對應主題值、bare value 或任意值:

@theme {
  --tab-size-2: 2;
  --tab-size-4: 4;
}

@utility tab-* {
  tab-size: --value([integer]);    /* 任意值:tab-[76] */
  tab-size: --value(integer);      /* bare value:tab-76 */
  tab-size: --value(--tab-size-*); /* 主題值:tab-2、tab-4 */
}

@variant 與 @custom-variant:套用 vs 定義

這兩個名字很像的指令,職責其實相反,一定要分清楚:@variant套用既有變體條件到某段 CSS;@custom-variant定義一個新的變體前綴。

先看 @variant——它讓你在自訂 CSS 規則裡直接套用 Tailwind 的變體條件(如 darkhover):

.my-panel {
  background: white;
  @variant dark {
    background: black;   /* dark 模式時才套用 */
  }
}

再看 @custom-variant——它讓你造出全新的變體前綴,是 v4 取代 v3 addVariant() JavaScript 外掛的做法:

@import "tailwindcss";

/* 定義 dark(基於 .dark class 的策略)*/
@custom-variant dark (&:where(.dark, .dark *));

/* 特性查詢變體 */
@custom-variant supports-grid (@supports (display: grid) { & });

/* 減少動態偏好 */
@custom-variant motion-reduce (@media (prefers-reduced-motion: reduce) { & });

/* data 屬性變體 */
@custom-variant data-loading (&[data-loading]);

定義好之後,這些前綴就能像內建變體一樣直接在 HTML 使用:

<button class="bg-blue-600 data-loading:opacity-50 data-loading:cursor-wait"
        data-loading>
  儲存中...
</button>

補充一點實務上的判斷:什麼時候用 @variant、什麼時候直接在 HTML 用變體前綴就好?如果這個「條件樣式」只出現在某個特定 class 內、而且用純 utility 不好表達(例如要在 @variant dark 裡改一個非標準屬性、或搭配複雜的計算值),那 @variant 讓你把邏輯收在 CSS 裡很乾淨。反之,如果只是 dark:bg-gray-900 這種單純的變體套 utility,就沒必要動用 @variant,直接在 HTML 寫變體前綴更直觀。@variant 的定位是「HTML 變體前綴的 CSS 版補充」,不是取代它。

一句話記住兩者的分工:@custom-variant 先造出前綴,@variant 在 CSS 裡臨場套用前綴的條件。

theme()、–alpha()、–spacing():三個函式

v4 提供三個能寫在 CSS 屬性值裡的函式。先看 v4 新增的兩個 -- 函式:--spacing() 依主題的 --spacing 基準推導間距,--alpha() 調整顏色不透明度。

.hero {
  /* --spacing(4) 等同 calc(var(--spacing) * 4) */
  margin: --spacing(4);

  /* --alpha() 調整不透明度,編譯成 color-mix() */
  color: --alpha(var(--color-lime-300) / 50%);
  /* → color-mix(in oklab, var(--color-lime-300) 50%, transparent) */
}

再看 theme()。它能引用 @theme 定義的 Token,但在 v4 有個重要轉變:大多數情況直接用 var(--...) 更簡潔theme() 主要保留給那些不支援 CSS 變數的場景,例如 @media 條件與 @keyframes

/* ✅ v4 推薦:直接用 CSS 變數引用 Token */
.hero { background-color: var(--color-blue-500); }

/* ✅ theme() 的正當用途:@media 條件不能放 CSS 變數,必須用 theme() */
@media (min-width: theme(--breakpoint-md)) {
  .sidebar { display: block; }
}

/* ✅ @keyframes 內同理 */
@keyframes fade {
  0%, 100% { opacity: 1; }
  50%      { opacity: theme(--opacity-50); }
}

為什麼 v4 要把大部分取值場景從 theme() 導向 var()?關鍵在於「執行時機」。theme()編譯期函式,Tailwind 在建構時把它替換成實際值,替換完就是死的字串;而 var(--color-blue-500)執行期的原生 CSS 變數,值會活在瀏覽器裡,可以被 .dark 之類的選擇器動態覆蓋,也能被 JavaScript 讀寫。這正好呼應上一篇《配置系統》講的「設定即變數」——既然 @theme 的 Token 本來就是真正的 CSS 變數,那用 var() 直接取用最自然、也最有彈性。theme() 之所以還留著,是因為 @media 條件與 @keyframes 這些地方在語法上就不接受 CSS 變數,這時才輪到編譯期的 theme() 上場。

這裡還有個 v3 遺留的坑要特別提:v3 的 theme() 用的是點記法字串(如 theme('colors.blue.500')),v4 改成直接引用 CSS 變數名、且不加引號(如 theme(--color-blue-500))。下面這張對照表把常見的 v3 → v4 函式轉換整理清楚:

v3 寫法v4 寫法
theme('spacing.4')--spacing(4)
theme('colors.blue.500 / 75%')--alpha(var(--color-blue-500) / 75%)
theme('colors.blue.500')var(--color-blue-500)(優先)或 theme(--color-blue-500)
screen(md)@media (width >= theme(--breakpoint-md))@variant md

常見錯誤與最佳實踐

最容易踩的坑

坑一(頭號殺手):@apply 濫用。 這是最常見的問題。TailwindCSS 作者 Adam Wathan 早就提醒過 @apply 的三個代價:一是重新引入命名問題,utility-first 的核心優勢就是不用替每個樣式取名,@apply 卻逼你回頭想「這個元件叫什麼」;二是失去可見性,看到 <button class="btn-primary"> 你無法立刻知道它套了哪些樣式,得跳去 CSS 檔查;三是CSS 膨脹.btn-primary.btn-secondary 即使有一堆相同的 utility,@apply 也會各自展開、不去重。所以在 React、Vue 這類場景,優先用元件封裝而非 @apply

坑二:@apply 循環引用導致建構失敗。 一個 class 透過 @apply 引用了會反過來引用自己的另一個 class,形成無限循環:

/* ❌ 循環引用:建構會直接失敗 */
@layer components {
  .btn      { @apply btn-base; }
  .btn-base { @apply btn; }
}

/* ✅ 正確:基礎 class 只用 utility,衍生 class 才引用基礎 class */
@layer components {
  .btn         { @apply px-4 py-2 rounded-md; }
  .btn-primary { @apply btn bg-blue-600 text-white; }
}

坑三:把自訂 CSS 寫在 @layer 之外。 沒放進 layer 的普通 CSS 優先級比所有 utility 都高,你會發現 HTML 裡的 utility「莫名其妙蓋不掉」。解法就是把它包進 @layer components(或適當的層)。

坑四:在 v4 沿用 v3 的 theme() 字串語法。color: theme('colors.blue.500') 在 v4 會警告,正確寫法是 var(--color-blue-500)(優先)或 theme(--color-blue-500)(不加引號、用變數名)。

坑五:在元件自有 CSS 檔用 @apply 卻忘了 @reference Vue SFC、CSS Module 這類檔案不會自動知道主題定義,少了 @reference "你的入口.css" 這行,@apply 會找不到 class 而失敗。

最佳實踐

  • 一律用 @import "tailwindcss":別再留 v3 的三行 @tailwind,一行搞定並帶進正確層級。
  • 自訂樣式一定進 @layerbase 放 HTML 元素樣式、components 放可重用元件,讓 utilities 能正常覆蓋。
  • 自訂 utility 用 @utility:自動支援全部變體,別再用 v3 的 @layer utilities 手寫 class。
  • @apply 有節制地用:「能用元件封裝就用元件,不能才用 @apply」,主要留給第三方 HTML、Markdown 輸出、無框架靜態站。
  • 取用 Token 優先 var(--...)theme() 只留給 @media@keyframes 這些不吃 CSS 變數的場景。

一句話總結:入口用 @import、優先級靠 @layer、自訂 utility 用 @utility、變體用 @custom-variant、取值優先 var()——而 @apply 能少用就少用。

小結

這是 TailwindCSS 完整教學 系列的第五篇。上一篇《配置系統》我們用 @theme 定義了設計 Token——決定「值從哪裡來」;這一篇則深入指令與函式——決定「你怎麼在 CSS 裡動用這些值」。回顧幾個重點:

  • 入口與層級:v4 用一行 @import "tailwindcss" 取代 v3 的三行 @tailwind,它展開成 theme < base < components < utilities 四個原生 Cascade Layer,優先級由層級順序決定、與特異性無關。
  • 提取與自訂@apply 語法 v3/v4 不變,但要節制使用;v4 新增的 @utility 是自訂 utility 的正確方式,自動支援所有變體,取代 v3 的 @layer utilities
  • 變體指令@custom-variant 定義新前綴(取代 v3 的 addVariant()),@variant 在 CSS 裡套用既有變體條件——一個造、一個用。
  • 三個函式--spacing() 推導間距、--alpha() 調不透明度、theme() 取用 Token;v4 更偏好直接 var(--...)theme() 主要留給 @media@keyframes

掌握了配置(值從哪來)與指令函式(怎麼動用值),你已經握有 v4 CSS-first 的完整語彙。下一篇 《TailwindCSS v4 遷移指南》,我們會把前面幾篇的差異點收攏起來,實際走一遍從 v3 升級到 v4 的完整流程——包含 @tailwind@importtailwind.config.js 搬進 @themeaddUtilities()/addVariant() 改寫成 @utility/@custom-variant,以及那些遷移時最容易卡住的細節。

BenZ Software Developer

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

本週主打

AI 自動化入門包

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

看看這個產品 →