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

本文是「使用 shadcn/ui 前該補的 TailwindCSS 基礎知識」系列文章的第六篇
系列文章:
在使用過 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 將元件的樣式分為三個部分:
- 基礎樣式 (Base):所有變體都共用的樣式
- 變體樣式 (Variants):不同變體的專屬樣式
- 複合變體 (Compound Variants):當多個變體組合時的特殊樣式(選用)
實際應用:Button 元件
以 shadcn/ui 的 Button 元件為例,來看看 CVA 如何使用:
import { cva, type VariantProps } from "class-variance-authority";
const buttonVariants = cva(
// 基礎樣式:所有按鈕都會套用
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50",
{
variants: {
// variant 變體:定義不同的外觀
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-white hover:bg-destructive/90",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
// size 變體:定義不同的尺寸
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3",
lg: "h-10 rounded-md px-6",
icon: "size-9",
},
},
// 預設值:當使用者沒有指定時使用
defaultVariants: {
variant: "default",
size: "default",
},
}
);
定義好 buttonVariants 後,在元件中使用:
function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
// 使用範例
<Button>預設 按鈕</Button>
<Button variant="destructive" size="lg">大尺寸刪除按鈕</Button>
<Button variant="ghost" size="icon"><Icon /></Button>
CVA 的核心優勢:
- 型別安全:TypeScript 會自動推斷可用的變體選項,寫錯會直接報錯
- 易於維護:所有樣式集中在一個地方,修改時不用到處找
- 靈活組合:可以輕鬆組合不同的變體,不用手動處理邏輯
- 預設值支援:可以設定預設的變體,簡化使用
進階功能:compoundVariants
CVA 還支援 compoundVariants,用於定義「當多個變體組合時的特殊樣式」。例如:
const buttonVariants = cva("base-styles", {
variants: {
variant: {
primary: "bg-blue-500",
secondary: "bg-gray-500",
},
size: {
sm: "text-sm",
lg: "text-lg",
},
outlined: {
true: "border-2",
false: "",
},
},
// 複合變體:當多個條件同時滿足時套用
compoundVariants: [
{
// 當 variant="primary" 且 outlined=true 時
variant: "primary",
outlined: true,
class: "border-blue-500 bg-transparent text-blue-500 hover:bg-blue-50",
},
{
// 當 size="lg" 且 outlined=true 時
size: "lg",
outlined: true,
class: "border-4", // 大尺寸的外框按鈕使用更粗的邊框
},
],
defaultVariants: {
variant: "primary",
size: "sm",
outlined: false,
},
});
使用範例:
// 會套用 primary + outlined 的複合變體樣式
<Button variant="primary" outlined={true}>
Primary Outlined Button
</Button>
// 會套用 lg + outlined 的複合變體樣式
<Button size="lg" outlined={true}>
Large Outlined Button
</Button>
// 會同時套用兩個複合變體的樣式
<Button variant="primary" size="lg" outlined={true}>
Large Primary Outlined Button
</Button>
components.json:shadcn/ui 的配置中心
components.json 是 shadcn/ui CLI 的核心配置檔。當使用 npx shadcn@latest add button 安裝元件時,CLI 會讀取這個檔案來決定:
- 元件應該安裝到哪個目錄
- 使用什麼樣的風格(default 或 new-york)
- 是否使用 TypeScript
- 路徑別名是什麼
- 是否使用 CSS 變數
- 從哪些 registry 安裝元件
components.json 的完整結構
以下是一個完整的 components.json 範例:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
各欄位說明
$schema
"$schema": "https://ui.shadcn.com/schema.json"
指向 shadcn/ui 的 JSON schema,提供 IDE 自動完成功能和欄位驗證。
style
"style": "new-york"// 或 "default"
選擇元件風格。shadcn/ui 提供兩種預設風格:
- default:較為簡潔的設計
- new-york:更現代、更精緻的設計
不同風格的元件在視覺設計和細節上有所不同,但功能完全相同。
rsc
"rsc": false
是否使用 React Server Components(Next.js 13+ 的功能)。如果專案使用 Next.js App Router,可以設為 true。
tsx
"tsx": true
是否使用 TypeScript。設為 true 時,安裝的元件會是 .tsx 檔案;設為 false 則是 .jsx 檔案。
tailwind
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
}
TailwindCSS 相關配置:
- config:tailwind.config 檔案的位置(TailwindCSS v4 通常不需要此檔案,可留空)
- css:CSS 入口檔案的位置,CLI 會在這個檔案中注入必要的 CSS 變數
- baseColor:基礎顏色主題,可選:
zinc、slate、stone、gray、neutral - cssVariables:是否使用 CSS 變數(強烈建議開啟,支援暗色模式和主題切換)
- prefix:TailwindCSS class 的前綴(例如設為
"tw-"後,所有 class 都會變成tw-flex、tw-p-4等)
iconLibrary
"iconLibrary": "lucide"
指定使用的圖示庫。shadcn/ui 預設使用 Lucide Icons。
aliases
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
路徑別名配置,告訴 CLI 各種檔案應該安裝到哪裡:
- components:元件的根目錄
- utils:工具函數的位置(
cn函數會安裝在這裡) - ui:UI 元件的目標目錄
- lib:函式庫目錄
- hooks:自訂 hooks 的目錄
這些別名必須與專案的 tsconfig.json 或 jsconfig.json 中的 paths 設定一致。
registries:自訂元件來源
registries 是 components.json 中最強大但也最容易被忽略的功能。它允許從多個來源安裝元件,包括:
- shadcn/ui 官方 registry
- 第三方 registry(如 v0.dev、Magic UI)
- 私有 registry(公司內部的元件庫)
- 自建 registry
基本配置
{
"registries": {
"@v0": "https://v0.dev/chat/b/{name}",
"@magicui": "https://magicui.design/r/{name}.json",
"@acme": "https://registry.acme.com/{name}.json"
}
}
{name} 會被替換成元件名稱。例如執行:
npx shadcn@latest add @v0/dashboard
CLI 會從 https://v0.dev/chat/b/dashboard 下載元件。
進階配置:帶認證的私有 registry
{
"registries": {
"@company": {
"url": "https://registry.company.com/ui/{name}.json",
"headers": {
"Authorization": "Bearer ${REGISTRY_TOKEN}",
"X-API-Key": "${API_KEY}"
},
"params": {
"version": "latest",
"team": "frontend"
}
}
}
}
環境變數(${VAR_NAME} 格式)會自動從系統環境變數中讀取。
使用方式:
# 設定環境變數export REGISTRY_TOKEN="your-token"
export API_KEY="your-api-key"
# 安裝私有元件
npx shadcn@latest add @company/custom-button
多 registry 混合使用
{
"registries": {
"@shadcn": "https://ui.shadcn.com/r/{name}.json",
"@v0": "https://v0.dev/chat/b/{name}",
"@company": {
"url": "https://registry.company.com/{name}.json",
"headers": {
"Authorization": "Bearer ${COMPANY_TOKEN}"
}
},
"@team": {
"url": "https://team.company.com/{name}.json",
"params": {
"team": "frontend",
"version": "${REGISTRY_VERSION}"
}
}
}
}
這樣就可以從不同來源安裝元件:
# 從 shadcn/ui 官方安裝
npx shadcn@latest add @shadcn/button
# 從 v0.dev 安裝
npx shadcn@latest add @v0/dashboard
# 從公司內部 registry 安裝
npx shadcn@latest add @company/auth-form
# 從團隊 registry 安裝
npx shadcn@latest add @team/data-table
components.json 與 shadcn CLI 的關係 components.json 是 shadcn CLI 的「設定檔」,CLI 會根據這個檔案:
- 決定安裝位置:根據
aliases決定檔案要放在哪裡- 選擇元件風格:根據
style下載對應風格的元件- 處理依賴:自動安裝元件所需的 npm 套件
- 注入 CSS 變數:在
tailwind.css中注入必要的 CSS 變數- 解析 registry:從指定的 registry 下載元件