【App 工程師的前端轉生術 06】樣式系統篇:Tailwind CSS 是你的 Modifier,不是傳統 CSS
01. 前言:別再寫 .css 檔案了
回想一下你在寫 Native App UI 時的習慣:
- iOS (SwiftUI): 你不會去寫一個獨立的 XML 檔案然後 import 進來,你會直接在 View 上面加
.padding().background(.red)。 - Android (Compose): 你不會去寫
styles.xml,你會直接傳入Modifier.padding().background(Color.Red)。
但在傳統 Web 開發中,CSS 是一個與 HTML 分離的檔案,這讓你覺得像是在寫古老的 layout.xml 加上 styles.xml,既難以維護又容易發生命名衝突(Global Scope Pollution)。
這一篇要介紹的 Tailwind CSS,其實就是 Web 界的 Modifier 系統。它的核心理念與現代 App UI 框架完全一致:將樣式與結構緊密結合 (Co-location)。
02. 觀念轉換:Utility Classes = Modifiers
Tailwind 不是讓你寫 CSS,而是提供了一組預定義的「原子類別」。
視覺化對比
讓我們要把一個按鈕變成:藍色背景、白色文字、圓角、內距 16px。
Android (Jetpack Compose):
Button(
modifier = Modifier
.background(Color.Blue)
.padding(16.dp)
.clip(RoundedCornerShape(8.dp))
) { Text("Click Me", color = Color.White) }
iOS (SwiftUI):
Text("Click Me")
.foregroundColor(.white)
.padding(16)
.background(Color.blue)
.cornerRadius(8)
Next.js (Tailwind CSS):
<button className="bg-blue-500 text-white p-4 rounded-lg">
Click Me
</button>
App 工程師視角:
發現了嗎?bg-blue-500 就是 .background(Color.blue),p-4 就是 .padding(16)。你不需要去想 CSS class 的命名(該叫 .btn-primary 還是 .blue-button?),你只需要組合這些 Modifiers。
03. 響應式設計 (RWD):Size Classes 的極致簡化
Mobile App 開發者通常需要處理 Pad vs Phone 的佈局。
- iOS: 使用
UserInterfaceSizeClass(.compact,.regular)。 - Android: 使用
BoxWithConstraints或資源限定符 (layout-w600dp)。
Tailwind 使用 Prefix (前綴) 來處理斷點(Breakpoints)。觀念是 "Mobile First"(預設是手機樣式,針對大螢幕做覆寫)。
範例:手機版垂直排列,平板版水平排列
// 預設 (手機): flex-col (VStack / Column)
// md (平板以上): flex-row (HStack / Row)
<div className="flex flex-col md:flex-row gap-4">
<div className="w-full md:w-1/2 bg-red-100">左側內容</div>
<div className="w-full md:w-1/2 bg-blue-100">右側內容</div>
</div>
這比 Android 的 layout-land 資料夾或是 iOS 的 Trait Collection 判斷要直觀非常多。你不需要切換檔案,所有狀態都在同一行代碼裡。
04. 深色模式 (Dark Mode):沒有 Assets.xcassets 怎麼辦?
在 App 中,我們習慣在 Assets 裡設定 Light/Dark 兩種顏色。
在 Tailwind 中,這同樣是透過 Prefix 解決:dark:。
<div className="bg-white dark:bg-gray-900 text-black dark:text-white p-4">
<h1>自動適應系統主題</h1>
<p className="text-gray-500 dark:text-gray-400">
這行字的顏色在深色模式下會變淺一點
</p>
</div>
只要在 tailwind.config.ts 設定好 darkMode: 'media' (跟隨系統) 或 'class' (手動切換),這些修飾符就會自動生效。
05. 進階技巧:解決 Modifier 衝突 (tailwind-merge)
在 Part 5 我們封裝了 Component。但如果外部使用者想要覆寫樣式怎麼辦?
情境: PrimaryButton 預設是 bg-blue-500,但我某個頁面想要它是紅色的。
// ❌ 錯誤做法:字串相加
// 結果:className="bg-blue-500 bg-red-500"
// CSS 規則是看定義順序,而不是 class 出現順序,所以結果不可預測!
<PrimaryButton className="bg-red-500" />
✅ 正確做法:使用 tailwind-merge
這就像是處理 Modifier 的順序問題。我們需要一個工具來「合併」並「剔除」衝突的樣式。
安裝工具:
npm install tailwind-merge clsx
建立一個 Utility Function (src/lib/utils.ts):
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
// 這就像是一個強化的 Utility Function,幫你處理字串合併
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
重構 PrimaryButton:
export function PrimaryButton({ className, ...props }: Props) {
return (
<button
className={cn(
"bg-blue-500 p-4 rounded-lg", // 預設值
className // 外部傳入的值
)}
{...props}
/>
)
}
現在,cn("bg-blue-500", "bg-red-500") 會正確輸出 "bg-red-500",藍色被安全地移除了。
06. 小結:UI 開發的完全體
這一篇我們補完了 View 層的最後一塊拼圖:樣式系統。
現在回頭看,我們已經把前端開發變成了你熟悉的形狀:
- 結構: Atomic Components $\approx$ Custom Views。
- 樣式: Tailwind CSS $\approx$ Modifiers。
- 邏輯: Custom Hooks $\approx$ ViewModels。
- 資料: Repository & Zod $\approx$ Codable Services。
到目前為止,我們都是在處理「單一頁面」或「單一功能」的開發。但在真實的 App 中,我們往往需要 跨頁面共享狀態(例如:使用者登入資訊、購物車內容)。
在 Native App,我們會用 Singleton 或 Dependency Injection。在 Next.js 裡,Singleton 是危險的(因為 Server 端會污染)。
在下一篇 【Part 7】,我們將探討 全域狀態管理。為什麼我推薦 Zustand 而不是 Redux?它跟 Android 的 Repository 模式有多像?讓我們繼續看下去。

關於作者
Ken Huang
熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。
最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。
這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。
系列文章目錄
- 【App 工程師的前端轉生術 04】邏輯層的核心:Custom Hooks 就是你的 ViewModel
- 【App 工程師的前端轉生術 05】UI 架構篇:原子化元件設計與 Slot Pattern
- 【App 工程師的前端轉生術 06】樣式系統篇:Tailwind CSS 是你的 Modifier,不是傳統 CSS (本文)
- 【App 工程師的前端轉生術 07】全域狀態管理:為什麼 Singleton 在這裡行不通?(Zustand)
- 【App 工程師的前端轉生術 08】前端也能寫後端:BFF 模式與 Server Actions