CSS 有一個根本問題:它的作用域是全域的。你寫 .button { color: blue } ,這個規則影響所有帶有 button class 的元素。解法出現過很多:BEM 命名規範、CSS Modules、CSS-in-JS——每一種都在嘗試解決「怎麼讓 CSS 的範圍可控」這個問題。
TailwindCSS 選擇了另一條路:不用語義化 class,直接用描述視覺效果的 utility class。樣式和元件放在一起,沒有命名問題,也不用擔心改一個 class 影響其他地方。
島島(DaoDao)的 packages/ui 和兩個 Next.js app,以及 NobodyClimb 的 Next.js web 端,都用 TailwindCSS 做樣式。
核心概念
Utility-first 的意思是:與其寫 .card { padding: 16px; border-radius: 8px; background: white },你直接在 HTML/JSX 裡用已有的 utility class:
// 傳統 CSS 方式
<div className="card">
<h2 className="card-title">標題</h2>
</div>
// Tailwind 方式
<div className="p-4 rounded-lg bg-white shadow-sm border border-gray-200">
<h2 className="text-xl font-bold text-gray-900">標題</h2>
</div>
第一眼看 Tailwind 的寫法,很多人的反應是「這跟 inline style 有什麼差」。差別在幾個地方:
設計系統約束:Tailwind 的 spacing scale(p-1 = 4px, p-2 = 8px, p-4 = 16px…)、color palette、font size 是預先定義好的,你只能從這個系統裡選,不容易寫出 padding: 13px 這種破壞一致性的值。
Responsive 和狀態:md:flex、hover:bg-blue-600、dark:text-white 這類修飾符直接加在 class 上,不需要寫 media query 或 pseudo-class:
<button className="
bg-blue-500 text-white px-4 py-2 rounded
hover:bg-blue-600
focus:outline-none focus:ring-2 focus:ring-blue-500
dark:bg-blue-400
md:px-6
">
送出
</button>
Build-time purge:Tailwind v3+ 預設只輸出你實際用到的 class,生產環境的 CSS 通常只有 5-20 KB,不管你的 Tailwind 設定有多少 token。
實際的使用模式
在 React 元件裡,重複的 class 組合用變數或函式抽出:
import { cn } from "@/lib/utils"; // clsx + tailwind-merge 的包裝
// 基本用法
function Card({ className, children }: CardProps) {
return (
<div className={cn("rounded-lg border bg-white p-4 shadow-sm", className)}>
{children}
</div>
);
}
// 有 variant 的元件
function Badge({ variant = "default", children }: BadgeProps) {
const variantClasses = {
default: "bg-blue-100 text-blue-800",
success: "bg-green-100 text-green-800",
warning: "bg-yellow-100 text-yellow-800",
danger: "bg-red-100 text-red-800",
};
return (
<span className={cn("rounded-full px-2 py-1 text-xs font-medium", variantClasses[variant])}>
{children}
</span>
);
}
cn 函式(通常是 clsx + tailwind-merge 組合)解決兩個問題:一是條件式 class 的組合,二是 class 衝突(例如 p-4 和 p-2 同時存在時,merge 確保後者勝出):
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
設定客製化
tailwind.config.js(或 Tailwind v4 的 CSS config)讓你擴充或覆蓋設計系統:
// tailwind.config.js
module.exports = {
content: ["./src/**/*.{ts,tsx}"],
theme: {
extend: {
colors: {
brand: {
50: "#eff6ff",
500: "#3b82f6",
900: "#1e3a8a",
},
},
fontFamily: {
sans: ["Inter", "sans-serif"],
},
borderRadius: {
xl: "12px",
"2xl": "16px",
},
},
},
};
擴充後就可以用 text-brand-500、font-sans、rounded-2xl 這些自訂 token。
Tailwind v4 的變化
Tailwind v4(2025 年發布)把設定從 JS 移到 CSS:
/* app.css */
@import "tailwindcss";
@theme {
--color-brand: #3b82f6;
--font-sans: "Inter", sans-serif;
}
不再需要 tailwind.config.js,設定放在 CSS 檔案裡,更接近 CSS 原生的設計理念。shadcn/ui 的最新版本也跟著遷移到 v4。
需要注意的地方
Class 順序問題:hover:bg-blue-500 bg-blue-600 和 bg-blue-600 hover:bg-blue-500 在 Tailwind 的輸出裡結果一樣,但如果你同時有 p-4 和 p-2 在不同條件下,不用 tailwind-merge 的話,哪個生效取決於 CSS 的 class 順序,而不是 JSX 裡的順序。
Linter 的 class 排序:class 一多,順序各自為政會讓程式碼難讀。prettier-plugin-tailwindcss 可以自動排序,讓同樣語義的 class 總是在同一個位置。
語義化 class 缺失:Tailwind 的 class 名稱是視覺描述(text-blue-500),不是語義(text-primary)。如果設計系統改了主色,你需要找遍所有 text-blue-500 替換,除非你用 CSS variables 包一層(shadcn/ui 的做法)。
HTML 的可讀性:元件帶十幾個 class 是常態,對初次看程式碼的人不友善。解法是把相關的 class 抽成元件,而不是把所有樣式堆在同一個 JSX 元素上。
整體來說
TailwindCSS 的爭議通常集中在「class 太多、太醜」這個點,但這其實是次要問題。主要問題是:CSS 的全域命名和死碼,Tailwind 都解了。
在 React 生態裡,Tailwind 和元件化思維是天生搭配——每個元件管自己的 class,沒有全域衝突,build 時自動 tree-shake,設計系統的約束讓視覺一致性可控。
如果你在一個 React 專案裡還在用傳統 CSS class,值得認真考慮遷移。