面試:技術題 — TailwindCSS 實作考題精選 | TailwindCSS 完整教學

2026/09/17
面試:技術題 — TailwindCSS 實作考題精選 | TailwindCSS 完整教學

走完《面試:核心概念》的「為什麼」,這一篇我們把焦點轉到 「怎麼做」。技術面試官不會只問你哲學,他會丟出實作題:「@theme 和舊的 config 差在哪」「怎麼寫一個自訂 utility」「變體堆疊的順序是怎麼運作的」「容器查詢和媒體查詢何時各用」「group / peer / has 怎麼玩」「暗色模式有幾種策略」「@apply 什麼時候該用」。本篇用 Q&A 形式精選 10 題技術實作題,每題都給你「題目 → 解析(含可執行程式碼) → 延伸追問」,幫你把「會寫」升級成「講得出原理」。

前言

上一篇《面試:核心概念》我們把 Tailwind 的哲學、爭議與運作原理整理成能一口氣講清楚的答案。但技術面試從來不會停在觀念——聊完「為什麼用 Tailwind」,面試官接著就會問「那你實際怎麼做」。

技術實作題考的是手感加原理:你不只要寫得出 @container 的響應式卡片、@utility 的自訂工具、group-has: 的父子連動,還要能說出它們背後怎麼編譯、為什麼這樣設計、有哪些坑。很多人天天寫 dark:hover:bg-blue-500,卻答不出「變體堆疊的順序是怎麼套用的」;天天用 @apply,卻講不清「什麼時候該用、什麼時候是反模式」。這些細節,正是技術面試拉開差距的地方。

本篇聚焦 TailwindCSS v4 的實作考題,適合正在準備前端面試的 Mid-level 到 Senior 工程師,也適合已經在用 Tailwind、想把「會寫」淬鍊成「講得出道理」的人。本篇你將學到:

  • 設定與擴充:@theme vs tailwind.config.js 的差異、如何擴充色彩系統、如何用 @utility 自訂工具
  • 響應式與變體:媒體查詢 vs 容器查詢的選擇、變體堆疊的順序機制、任意值(arbitrary values)的正確用法
  • 關聯選擇器:grouppeerhas 三種父子/兄弟連動的差異與實戰
  • 暗色模式:media / class / data 三種策略的取捨,以及 @apply 的正確與錯誤使用時機

面試題精選

以下 10 題由淺入深:先從最高頻的「@theme vs config」與色彩擴充打底,再進入自訂 utility、變體堆疊與任意值,接著深入容器查詢與 group / peer / has 的關聯選擇器,最後以暗色模式策略與 @apply 時機收束。每題都附解析(含可執行程式碼)與延伸追問——延伸追問是面試官順著你答案往下挖的常見問題,先想過就不會被問倒。

題目 1:v4 的 @theme 和 v3 的 tailwind.config.js 差在哪?

解析

這是 v4 上線後最高頻的技術題。核心答案是:設定的所在地從 JavaScript 搬回了 CSS。

v3 用 tailwind.config.js 這個 JS 檔定義主題;v4 改用 CSS 原生的 @theme directive,直接在 CSS 裡定義設計 Token。

/* v4: app.css —— 設定寫在 CSS 裡 */
@import "tailwindcss";

@theme {
  --color-brand-500: #3b82f6;
  --font-display: "Inter", sans-serif;
  --spacing-18: 4.5rem;
  --breakpoint-3xl: 1920px;
}

對比 v3 的寫法:

// v3: tailwind.config.js —— 設定寫在 JS 裡
module.exports = {
  theme: {
    extend: {
      colors: { brand: { 500: '#3b82f6' } },
      fontFamily: { display: ['Inter', 'sans-serif'] },
      spacing: { 18: '4.5rem' },
      screens: { '3xl': '1920px' },
    },
  },
}

回答時務必點出 @theme 帶來的關鍵好處:它定義的每個 Token 會同時生成 utility class 真實的 CSS 變數。所以 --color-brand-500 一寫,你既能用 bg-brand-500text-brand-500,也能在自訂 CSS 裡 var(--color-brand-500)——一份定義,兩種用法,不再有 JS 設定和 CSS 世界的斷層。

<!-- @theme 定義後,utility 直接可用 -->
<div class="bg-brand-500 font-display p-18">品牌區塊</div>
/* 同一個 Token 也能在自訂 CSS 裡直接引用 */
.custom-badge {
  background: var(--color-brand-500);
}

延伸追問:「那舊專案的 tailwind.config.js 在 v4 還能用嗎?」——能。v4 保留 @config 指令,可在 CSS 頂端寫 @config "../../tailwind.config.js"; 載入舊的 JS 設定檔,方便漸進遷移。但新專案官方建議直接用 @theme,因為它更貼近 CSS 原生、值在 DevTools 裡直接可見可調試。

題目 2:如何擴充 Tailwind 的色彩系統?請寫出程式碼

解析

擴充色彩是最實務的設定題。v4 的做法是在 @theme 裡用 --color-{name}-{shade} 的命名慣例定義,Tailwind 會自動生成所有對應的 utility(bg-text-border-ring- 等)。

@import "tailwindcss";

@theme {
  /* 定義一整組品牌色階,命名遵循 --color-{name}-{shade} */
  --color-brand-50:  #eff6ff;
  --color-brand-100: #dbeafe;
  --color-brand-500: #3b82f6;
  --color-brand-600: #2563eb;
  --color-brand-900: #1e3a8a;

  /* 也能定義單一語意色 */
  --color-surface: #f8fafc;
}

定義後,所有色彩相關的 utility 立即可用,而且變體照樣支援:

<button class="bg-brand-500 hover:bg-brand-600 text-white
               border border-brand-600 ring-brand-500/40
               dark:bg-brand-900 px-4 py-2 rounded-md">
  品牌按鈕
</button>

進階技巧:v4 推薦用 OKLCH 色彩空間定義色階,能得到更均勻的視覺明度,這也是 v4 內建調色盤採用的格式。

@theme {
  /* 用 oklch 定義,明度過渡更自然 */
  --color-accent-500: oklch(0.72 0.18 195);
  --color-accent-600: oklch(0.64 0.19 195);
}

若要讓某個色階同時支援亮暗兩套值,可搭配 CSS 變數與 @theme inline:

:root {
  --brand: #3b82f6;
}
.dark {
  --brand: #60a5fa;
}
@theme inline {
  /* inline 讓 utility 直接引用外部變數,不複製值 */
  --color-brand: var(--brand);
}

延伸追問:「如果我只想覆寫預設調色盤的某個顏色,而不是新增呢?」——直接用同名 Token 覆寫即可,例如 --color-blue-500: #1d4ed8; 會替換掉內建的 blue-500。若想清空整組預設色只留自己的,可用 --color-*: initial; 先重置,再逐一定義——這招能有效縮小最終產出並強制團隊只用設計系統內的顏色。

題目 3:如何用 @utility 建立自訂 utility?它和 @apply 有何不同?

解析

v4 引入 @utility directive,讓你能定義真正的 utility class——它會被歸入 utilities layer、支援所有變體(hover:md:dark:),這是 @apply 做不到的。

@import "tailwindcss";

/* 定義一個自訂 utility:隱藏捲軸 */
@utility scrollbar-hide {
  &::-webkit-scrollbar {
    display: none;
  }
  scrollbar-width: none;
}

定義後,它就是一個一等公民 utility,能掛任何變體:

<!-- 自訂 utility 支援變體堆疊,和內建 utility 一樣 -->
<div class="overflow-x-auto scrollbar-hide md:scrollbar-hide">
  水平捲動內容...
</div>

@utility 還支援帶參數的動態 utility(functional utility),用 --value() 取值:

/* 定義可帶任意值的 utility:tab-* */
@utility tab-* {
  tab-size: --value(integer);
}
<pre class="tab-2">縮排兩格</pre>
<pre class="tab-[8]">縮排八格(任意值)</pre>

@apply 的關鍵差異要講清楚:@apply 是把既有 utility 的樣式「內聯貼進」某個一般 class(通常放在 components layer),它產出的是普通 class,無法自己再掛變體;而 @utility 產出的是真正的 utility,自動支援全套變體、優先序也對(在 utilities layer)。所以要做「可複用、可加變體」的原子工具,用 @utility;要做「元件級的樣式聚合」,才考慮 @apply(且多數時候該用元件)。

延伸追問:「那 @utility 定義的東西會不會被 tree-shaking 掉?」——會,而且這正是它的優點。@utility 定義的 class 只有在原始碼中真的被用到時才會出現在產出裡,沒用到的自動不生成,和內建 utility 享受同樣的 JIT 待遇。這也是它優於「手寫一段 .scrollbar-hide { ... } 塞進 CSS」的地方——後者會無條件打包。

題目 4:變體(variant)堆疊的順序是怎麼運作的?

解析

這題考的是對 dark:md:hover:bg-blue-500 這種多重變體如何編譯的理解。核心答案:變體由右往左、由內往外套用,最靠近 utility 的變體是最內層條件。

<!-- 讀法:在「暗色模式」下 & 視窗達「md 斷點」& 滑鼠「懸停」時,才套用藍底 -->
<button class="dark:md:hover:bg-blue-500 bg-gray-200 px-4 py-2 rounded">
  多重變體按鈕
</button>

它大致編譯成這樣的巢狀條件:

@media (prefers-color-scheme: dark) {
  @media (min-width: 768px) {
    .dark\:md\:hover\:bg-blue-500:hover {
      background-color: #3b82f6;
    }
  }
}

實務上這些條件多半是 AND 關係(必須全部成立),所以對「會不會生效」而言,dark:hover:hover:dark: 通常結果相同——順序不影響是否成立。但順序影響可讀性,團隊常約定一個固定順序(如「響應式 → 主題 → 狀態」)以利維護。

真正需要留意順序的,是變體涉及不同來源選擇器時。例如 group-hover: 作用於「父元素懸停」,peer-focus: 作用於「兄弟元素聚焦」——它們不是單純疊加在同一元素,而是分別對應不同的關聯選擇器:

<div class="group">
  <input class="peer" />
  <!-- 這個元素:當「祖先 .group 懸停」且「前面的 .peer 聚焦」時變色 -->
  <span class="group-hover:peer-focus:text-blue-600">狀態文字</span>
</div>

延伸追問:「那自訂變體怎麼做?例如我想要一個 not-first: 的效果。」——v4 內建了不少這類變體(如 not-first:first:last:),也能用 @custom-variant 自訂:@custom-variant pointer-fine (@media (pointer: fine)); 之後就能用 pointer-fine:hover:scale-105。能講出 @custom-variant 就展現了對 v4 擴充機制的掌握。

題目 5:任意值(Arbitrary Values)如何運作?何時該用、何時該避免?

解析

當預設設計 Token 不夠用時,可用方括號 [] 語法指定任何有效的 CSS 值——這就是任意值。它讓 Tailwind 在保有設計系統的同時,保留了「逃生艙口」。

<!-- 任意尺寸、顏色、間距 -->
<div class="w-[137px] h-[2.3rem] bg-[#1da1f2] mt-[13px]">固定尺寸</div>

<!-- 任意 grid 模板 -->
<div class="grid grid-cols-[1fr_2fr_1fr]">非均等三欄</div>

<!-- 引用 CSS 變數 -->
<div class="bg-[var(--brand)]">品牌色背景</div>

當 Tailwind 無法判斷值的型別時,要加型別提示(type hint):

<!-- 強制解釋為顏色 / 長度,避免被誤判 -->
<div class="bg-[color:var(--my-color)] text-[length:var(--font-size)]">
  型別提示
</div>

任意值不只用於「值」,也能用於變體選擇器(arbitrary variants):

<!-- [選擇器]:utility —— 對第 3 個子元素加底線 -->
<li class="[&:nth-child(3)]:underline">項目</li>

<!-- 對所有直接子代 li 加間距與底線 -->
<ul class="[&>li]:py-2 [&>li]:border-b">
  <li>第一項</li>
  <li>第二項</li>
</ul>

何時該用:精確像素定位(logo、第三方元件尺寸)、複雜 grid 模板、引用 CSS 變數、覆寫第三方樣式。何時該避免:用 text-[14px] 代替 text-smmt-[16px] 代替 mt-4——設計 Token 已有對應值時濫用任意值,會破壞一致性,也讓 CSS 因為每個不同任意值都是一條新規則而膨脹(上一篇談過的「去重」前提被打破)。

延伸追問:「大量重複用同一個任意值(如很多地方寫 w-[137px])怎麼辦?」——這正是訊號:該把它升級成設計 Token。在 @theme 裡加 --spacing-137: 137px; 或一個語意化的 --width-card: 137px;,之後用 w-card。任意值是「一次性例外」,反覆出現就代表它其實是設計系統的一員,該正式收編。

題目 6:容器查詢(Container Query)和媒體查詢差在哪?v4 怎麼寫?

解析

這是元件化時代的必考題。核心差異:媒體查詢根據「視窗(viewport)寬度」,容器查詢根據「父容器寬度」。後者讓元件能真正自適應,不依賴全局視窗尺寸。

經典情境:一張「產品卡」元件,放主區域要水平大卡、放側邊欄要垂直小卡。用媒體查詢無解——因為同一視窗尺寸下,主區域和側邊欄的卡片會套用相同樣式。容器查詢才能讓卡片依自己所在的容器決定佈局。

v4 內建容器查詢,不需額外外掛。在容器加 @container,子元素用 @md:@lg: 前綴:

<!-- 父層宣告 @container -->
<div class="@container">
  <!-- 依「父容器」寬度切換佈局,而非視窗 -->
  <div class="flex flex-col @md:flex-row gap-4 p-4">
    <img class="w-full @md:w-48 @md:shrink-0 aspect-video object-cover rounded-lg"
         src="product.jpg" alt="產品圖" />
    <div>
      <h3 class="text-base @lg:text-xl font-semibold">產品名稱</h3>
      <p class="text-sm text-gray-600 mt-1">產品描述...</p>
    </div>
  </div>
</div>

巢狀容器時,用命名容器避免混淆:

<div class="@container/sidebar">
  <div class="@container/card">
    <!-- @lg/sidebar 依 sidebar 容器,@md/card 依 card 容器 -->
    <div class="grid grid-cols-1 @lg/sidebar:grid-cols-2">...</div>
  </div>
</div>

何時選哪個:整體頁面佈局(側邊欄是否顯示)、導航從桌機到手機的切換,用媒體查詢;可複用元件(Card、List Item)、Widget、嵌入式元件,用容器查詢

延伸追問:「容器查詢還能根據容器的哪些維度?只有寬度嗎?」——除了寬度(size / inline-size),v4 也支援 @min-[...]@max-[...] 任意值容器斷點(如 @min-[475px]:flex),以及範圍區間(@min-md:@max-lg: 這類組合)。核心維度以 inline-size(通常即寬度)為主,這也是絕大多數響應式元件真正需要的。

題目 7:grouppeer 有什麼差別?各舉一個實戰範例

解析

grouppeer 是 Tailwind 用來表達「關聯狀態」的兩大變體:group 處理父子關係,peer 處理兄弟關係。

group——父元素狀態影響子元素:父加 group,子用 group-hover:group-focus: 等。

<!-- 懸停整張卡片時,標題與箭頭一起變色、箭頭右移 -->
<a href="#" class="group flex items-center gap-4 p-4 rounded-xl
                   bg-white border border-gray-200 hover:border-blue-300
                   transition-all duration-200">
  <div>
    <h3 class="font-semibold text-gray-900 group-hover:text-blue-700 transition-colors">
      前往下一步
    </h3>
    <p class="text-sm text-gray-500">點擊繼續操作</p>
  </div>
  <svg class="w-5 h-5 text-gray-400 ml-auto
              group-hover:text-blue-500 group-hover:translate-x-1
              transition-all duration-200"
       fill="none" viewBox="0 0 24 24" stroke="currentColor">
    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
  </svg>
</a>

peer——兄弟元素狀態影響後續兄弟:來源加 peer,後續兄弟用 peer-checked:peer-focus: 等。注意目標必須在 HTML 順序中位於 peer 之後(CSS 後續兄弟選擇器的限制)。

<!-- Toggle Switch:隱藏原生 checkbox,自訂外觀跟隨勾選狀態 -->
<label class="flex items-center gap-3 cursor-pointer">
  <input type="checkbox" class="sr-only peer" />
  <div class="w-11 h-6 rounded-full bg-gray-300 relative transition-colors
              peer-checked:bg-blue-500
              peer-focus-visible:ring-2 peer-focus-visible:ring-blue-500">
    <div class="absolute left-0.5 top-0.5 w-5 h-5 rounded-full bg-white shadow
                transition-transform peer-checked:translate-x-5"></div>
  </div>
  <span class="text-gray-700 peer-checked:text-blue-700 font-medium">啟用通知</span>
</label>

巢狀時用命名避免衝突:group/cardpeer/email,子元素用 group-hover/card:peer-invalid/email:

延伸追問:peer 為什麼目標一定要在來源之後?能不能反過來影響前面的兄弟?」——因為 peer-* 底層用的是 CSS 的一般後續兄弟選擇器 ~,它只能選到之後的兄弟。要影響「前面」的元素,傳統做不到,但這正好帶出下一題的 has:——用父層的 :has() 可以繞過方向限制。

題目 8:has 變體解決了什麼問題?怎麼用?

解析

has: 對應 CSS 的 :has() 選擇器,它讓父元素能根據「內部子孫的狀態」改變自己的樣式——這是 group/peer 都做不到的「由下往上」方向。

<!-- 當卡片內含被勾選的 checkbox 時,卡片本身變成藍色高亮邊框 -->
<label class="block p-4 rounded-xl border border-gray-200 cursor-pointer
              has-[:checked]:border-blue-500 has-[:checked]:bg-blue-50
              has-[:checked]:ring-2 has-[:checked]:ring-blue-500/30">
  <input type="checkbox" class="mr-2" />
  選我會讓整張卡片高亮
</label>

has: 常和 group 結合成 group-has:,讓祖先依「後代狀態」變化:

<!-- 表單區塊:當內部有任何 invalid 的輸入時,整區顯示錯誤提示樣式 -->
<fieldset class="group p-4 border rounded-lg">
  <input type="email" required
         class="border px-3 py-2 rounded w-full" placeholder="Email" />
  <!-- 只在 group 內含 invalid 輸入時才顯示 -->
  <p class="mt-2 text-sm text-red-500 hidden group-has-[:invalid]:block">
    表單中有欄位格式不正確
  </p>
</fieldset>

has: 也解了上一題 peer 的「只能影響後面」限制——因為它作用在父層,方向不受兄弟順序束縛:

<!-- 父層依「內部是否有聚焦的 input」加樣式,不管 input 在哪個位置 -->
<div class="rounded-lg border p-3 has-[input:focus]:ring-2 has-[input:focus]:ring-blue-500">
  <label class="text-sm text-gray-600">搜尋</label>
  <input class="mt-1 w-full outline-none" placeholder="輸入關鍵字" />
</div>

延伸追問:has: 有沒有效能或相容性顧慮?」——:has() 現在主流瀏覽器(Chrome、Safari、Firefox 較新版)都已支援,實務可用。效能上,現代引擎對 :has() 已有優化,但避免用在極深、極廣的選擇器上(如全域 body:has(...) 搭配大量子孫)以防重排成本。合理範圍(元件內、卡片層級)使用完全沒問題,它讓很多過去要靠 JS 才能做到的父子連動變成純 CSS。

題目 9:暗色模式有幾種實作策略?怎麼做「亮/暗/系統」三選一?

解析

dark: 變體的觸發策略主要有三種,面試要能講清楚各自的取捨:

  • media 策略(v4 預設):dark: 對應 @media (prefers-color-scheme: dark),完全跟隨系統設定,無法手動切換。適合不需要手動開關的簡單站點。
  • class 策略:用 .dark class 掛在 <html> 上決定,支援手動切換。這是需要「亮/暗/系統」切換器時的主流做法。
  • data 策略:用 data-theme="dark" 屬性,語意更明確,適合多主題(不只亮暗)場景。

v4 用 @custom-variant 設定 class 策略:

@import "tailwindcss";

/* 讓 dark: 改為由 .dark class 觸發(而非只跟系統) */
@custom-variant dark (&:where(.dark, .dark *));

接著實作「亮/暗/系統」三選一的核心邏輯:

type Theme = 'light' | 'dark' | 'system'
const KEY = 'theme-preference'

function systemPrefersDark(): boolean {
  return window.matchMedia('(prefers-color-scheme: dark)').matches
}

// 套用主題:system 時再問一次系統偏好
function applyTheme(theme: Theme): void {
  const isDark = theme === 'dark' || (theme === 'system' && systemPrefersDark())
  document.documentElement.classList.toggle('dark', isDark)
  document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'
}

export function setTheme(theme: Theme): void {
  localStorage.setItem(KEY, theme)
  applyTheme(theme)
}

system 時要監聽系統偏好變更,才能即時反應:

const mq = window.matchMedia('(prefers-color-scheme: dark)')
mq.addEventListener('change', () => {
  const saved = localStorage.getItem('theme-preference') as Theme | null
  if (!saved || saved === 'system') applyTheme('system')
})

最後別忘了防止 FOUC(主題閃爍)——在 <head> 內聯一段同步腳本,在頁面渲染前就決定好 .dark:

<script>
  (function () {
    var t = localStorage.getItem('theme-preference') || 'system'
    var dark = t === 'dark' ||
      (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
    if (dark) document.documentElement.classList.add('dark')
  })()
</script>

延伸追問:「為什麼 FOUC 的腳本一定要放在 <head> 且同步執行?」——因為若等到 React/Vue 掛載或外部 JS 載入後才加 .dark,使用者會先看到亮色版本「閃一下」再切暗,體驗很差。放在 <head>同步腳本能在瀏覽器繪製第一幀之前就決定主題,徹底消除閃爍。這也是為什麼這段腳本不能用非同步或模組化載入。

題目 10:@apply 的正確與錯誤使用時機是什麼?

解析

這是照妖鏡題——@apply 用得好不好,直接反映你有沒有真正理解 Tailwind 的哲學。核心立場:@apply 是「例外處理」,不是「日常習慣」。

@apply 把既有 utility 的樣式「內聯貼進」一個一般 class:

@import "tailwindcss";

/* 用 @apply 聚合成一個元件 class */
.btn-primary {
  @apply px-4 py-2 rounded-md font-medium bg-blue-600 text-white
         hover:bg-blue-700 transition-colors;
}

合理使用時機有三種:其一,無法控制 HTML——樣式來自 Markdown、CMS 或第三方渲染,只能靠 CSS 選擇器掛;其二,某組樣式在純靜態、無元件系統的專案裡大量原封不動地重複;其三,要覆寫第三方套件的既有 class。

/* 合理範例:替 Markdown 產出的內容套排版樣式(你改不了它的 HTML) */
.prose-custom h2 {
  @apply text-2xl font-bold mt-8 mb-4;
}
.prose-custom a {
  @apply text-blue-600 underline hover:text-blue-800;
}

錯誤使用更常見:在 React/Vue/Svelte 等元件化環境裡,把重複樣式用 @apply 抽成 .btn.card——這是反模式。因為重複的樣式應該用元件抽取,而不是回頭造語意化 CSS class:

// ✅ 正解:用元件封裝,而非 @apply 造 .btn
function Button({ children }) {
  return (
    <button className="px-4 py-2 rounded-md font-medium bg-blue-600 text-white
                       hover:bg-blue-700 transition-colors">
      {children}
    </button>
  )
}

一旦你用 @apply 造了一堆 .btn.card,就等於退回傳統 CSS 的老路——重新面對命名困難、全域作用域、特異性打架、「不敢刪」等 Tailwind 本來幫你繞開的問題,把核心優勢全丟了。

延伸追問:「那 @apply 和 v4 的 @utility 到底該怎麼選?」——判準是「你要的是元件還是原子工具」。要元件級的樣式聚合(一個 .btn 包含多屬性、通常不需再掛變體),且處在無元件系統的環境,才用 @apply;要做可複用、可加變體的原子 utility(如 scrollbar-hide,還要能 md:scrollbar-hide),就用 @utility——它產出真正的 utility、自動支援全套變體、優先序也對。能講清這條分野,面試官就知道你是真的懂 v4。

答題技巧與最佳實踐

技術實作題和觀念題不同——它更吃「你有沒有真的寫過」。分享幾個實戰答題策略。

先寫再講,程式碼會說話。 被問「怎麼做響應式卡片」,別只用嘴描述,主動說「我寫給你看」然後在白板上敲出 @container + @md:flex-row能當場寫出可執行程式碼的人,可信度遠高於只會口述概念的人。 寫完再用一兩句話解釋關鍵行為,層次就出來了。

主動點出坑與取捨。 每個實作題背後都有陷阱:任意值濫用會讓 CSS 膨脹、@apply 在元件環境是反模式、peer 只能影響後面的兄弟。每當你答完「怎麼做」,順手補一句「但要注意…」,立刻從「會用」跳升到「用得對」——這是資深的訊號。

用「v4 vs v3」展現版本敏感度。 很多實作在 v4 有了新解法:設定從 config 搬到 @theme、自訂工具從手寫 CSS 變成 @utility、暗色策略用 @custom-variant 設定。能自然帶出「v3 這樣、v4 改成這樣」,證明你在跟進生態演進,而不是停在幾年前的知識。

區分「值」和「機制」。 面試官問變體堆疊、容器查詢時,別只背語法,要能講出底層機制——變體會編譯成巢狀 media/選擇器、has: 對應 :has()、容器查詢對應 @container CSS at-rule。能把 utility 對應回它生成的原生 CSS,代表你不是只會抄 class。

收尾釘一句判準。 每題答完給一句能記住的取捨準則:「能用元件解決的就別用 @apply」「任意值是例外不是常態」「要加變體就用 @utility」。一句漂亮的判準,會讓面試官記住你這個答案。

小結

這是 TailwindCSS 完整教學 系列的第四十八篇,也是面試單元的第二篇。上一篇《面試:核心概念》我們把 Tailwind 的哲學、爭議與運作原理整理成講得清楚的觀念;這一篇則把焦點從「為什麼」轉到「怎麼做」,一題一題拆解最常考的實作題。回顧本篇的核心:

  • 設定與擴充:@theme 把設定從 JS 搬回 CSS,一份 Token 同時生成 utility 與 CSS 變數;色彩系統用 --color-{name}-{shade} 命名擴充,@utility 定義能掛全套變體的真正 utility。
  • 響應式與變體:變體「由右往左、由內往外」套用且編譯成巢狀條件;任意值是「逃生艙口」,反覆出現就該升級成 Token;容器查詢依父容器寬度、媒體查詢依視窗——元件用前者、頁面佈局用後者。
  • 關聯選擇器:group(父影響子)、peer(兄影響弟、只能往後)、has(子影響父、繞過方向限制)三者分工明確,group-has: 能組合出強大的父子連動。
  • 暗色模式與 @apply:media / class / data 三種策略對應不同切換需求,@custom-variant 設定 class 策略、<head> 同步腳本防 FOUC;@apply 是例外處理,元件環境該用元件而非造 .btn

技術關過了,面試最後一道硬骨頭是除錯。下一篇《面試:除錯題》,我們會把場景換成「線上出問題了,你怎麼查」——為什麼某個 class 沒生效、動態拼接的 class 為何消失、truncate 為何不作用、產出的 CSS 為何比預期大、樣式為何被意外覆蓋等真實故障,一題一題教你系統化定位與修復。把觀念、技術與除錯三關都補齊,Tailwind 的面試你就能穩穩接住。我們下篇見。

BenZ Software Developer

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

本週主打

AI 自動化入門包

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

看看這個產品 →