使用 shadcn/ui 前該補的 TailwindCSS 基礎知識(五) - shadcn/ui 生態系工具鏈

本文是「使用 shadcn/ui 前該補的 TailwindCSS 基礎知識」系列文章的第四篇
系列文章:
- 從 MUI 到 TailwindCSS 設計哲學的轉變
- 理解 TailwindCSS 的運作原理
- TailwindCSS v4 基礎語法速查
- 深入 TailwindCSS v4 的進階配置
- shadcn/ui 生態系工具鏈(本篇)
在使用過 shadcn/ui 的提供的程式碼後,會發現 shadcn/ui 並不是只有單純使用 Tailwind 而已,常會搭配幾個工具來解決在實際開發中會遇到的各種問題,例如:條件式樣式、className 衝突、元件變體管理等。以下將逐一介紹這些工具。
clsx 與 tailwind-merge:className 管理的最佳拍檔
clsx:條件式 className 組合
在寫 React 元件時,經常會遇到需要根據 props 或狀態來決定要套用哪些 className 的情況。例如:
- 按鈕有不同的變體(primary、secondary、outline)
- 元件有不同的尺寸(sm、md、lg)
- 根據狀態顯示不同樣式(active、disabled、loading)
如果用傳統的字串拼接,程式碼會變得很難維護:
// ❌ 難以維護的寫法
const className =
"btn" +
(isPrimary ? " btn-primary" : "") +
(isLarge ? " btn-large" : "") +
(isDisabled ? " btn-disabled" : "");
clsx 就是為了解決這個問題而生的工具。它讓你可以用更直觀的方式組合條件式的 className:
import clsx from "clsx";
// ✅ 清晰易讀的寫法
const className = clsx("btn", {
"btn-primary": isPrimary,
"btn-large": isLarge,
"btn-disabled": isDisabled,
});
tailwind-merge:解決 className 衝突
在建立可重複使用的元件時,經常會遇到一個棘手的問題:使用者傳入的 className 可能會與元件預設的 className 衝突。
例如,元件預設有 p-4 的內距,但使用者想要傳入 p-8 來覆蓋它。如果只用 clsx,兩個 className 都會存在,而實際套用哪個取決於 CSS 載入順序,結果不可預測:
// ❌ 問題:兩個 padding 都存在
<div className={clsx("p-4", customPadding)}>
{/* 如果 customPadding = 'p-8',p-4 和 p-8 都會存在 */}
{/* 實際套用哪個?不確定! */}
</div>
tailwind-merge 會智慧地判斷哪些 className 是衝突的,並保留後面的那一個:
import { twMerge } from "tailwind-merge";
// ✅ 解決方案:後面的覆蓋前面的
<div className={twMerge("p-4", "p-8")}>{/* 結果: "p-8"(p-4 被移除) */}</div>;
cn 函數:clsx + tailwind-merge 的完美組合
在實際開發中,通常會同時需要 clsx 的條件式組合能力和 tailwind-merge 的衝突解決能力。因此,shadcn/ui 定義了一個 cn 函數,結合了兩者的優勢。
在 shadcn/ui 的 utils.ts 中可以看到這個函數的定義:
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
這個簡單的函數先用 clsx 處理條件式組合,再用 tailwind-merge 解決衝突。使用者可以輕鬆地覆蓋預設樣式,而不用擔心 className 衝突的問題。
tw-animate-css:TailwindCSS v4 的動畫解決方案
在 TailwindCSS v4 推出後,原本廣泛使用的 tailwindcss-animate 插件因為基於舊的 JavaScript 插件系統而無法直接使用。tw-animate-css 就是為了解決這個問題而生的替代方案,它採用 TailwindCSS v4 的 CSS-first 架構,提供純 CSS 的動畫解決方案。
安裝與使用
npm install tw-animate-css
在 CSS 入口檔案中引入:
@import "tailwindcss";
@import "tw-animate-css";
基本用法
Enter/Exit 動畫:
// 淡入動畫
<div className="animate-in fade-in duration-500">
淡入效果
</div>
// 從上方滑入
<div className="animate-in slide-in-from-top duration-300">
從上方滑入
</div>
// 淡出動畫
<div className="animate-out fade-out duration-500">
淡出效果
</div>
// 組合多種效果
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-100">
延遲 100ms 後,從下方淡入滑入
</div>
動畫參數控制:
// 控制動畫時長
<div className="animate-in fade-in duration-150">快速淡入</div>
<div className="animate-in fade-in duration-1000">慢速淡入</div>
// 控制緩動函數
<div className="animate-in slide-in-from-left ease-in-out">
使用 ease-in-out
</div>
// 控制延遲
<div className="animate-in fade-in delay-500">延遲 500ms</div>
// 控制重複次數
<div className="animate-bounce repeat-infinite">無限彈跳</div>
<div className="animate-pulse repeat-3">重複 3 次</div>
// 控制方向
<div className="animate-bounce direction-alternate">來回彈跳</div>
現成的動畫:
// Accordion 動畫(常用於展開/收合元件)
<div className="animate-accordion-down">展開</div>
<div className="animate-accordion-up">收合</div>
// 閃爍游標(常用於輸入提示)
<span className="animate-caret-blink">|</span>
shadcn/ui 的許多元件都內建了動畫效果,這些動畫大多使用 tw-animate-css 或類似的動畫工具實作。例如:
- Accordion:使用
accordion-down和accordion-up - Dialog:使用
fade-in和slide-in-from-bottom - Dropdown Menu:使用
slide-in-from-top和fade-in - Toast:使用
slide-in-from-right和fade-in
可以查看 tw-animate-css GitHub 可以了解更多進階用法和完整的 API 文件。
Class Variance Authority (CVA):元件變體管理
在建立可重複使用的元件時,經常會遇到這樣的需求:
- 按鈕有不同的外觀變體(primary、secondary、outline、ghost)
- 每個變體有不同的尺寸(sm、md、lg)
- 需要組合這些變體(例如:大尺寸的 outline 按鈕)
如果用傳統的方式,需要寫很多 if-else 或 switch-case 來處理這些組合,程式碼會變得很難維護。
CVA (Class Variance Authority) 就是為了解決這個問題而生的工具。它讓我們可以用宣告式的方式定義元件的「基礎樣式」和「變體樣式」,然後根據 props 自動組合出正確的 className。
CVA 的核心概念
CVA 將元件的樣式分為三個部分: