圖文 + 多個場景案例詳解 shadcn + tailwind 顛覆性組件開發
整理版優先睇
shadcn/ui顛覆傳統:組合式API同代碼分發打造真正可定製組件庫
呢篇文章嚟自稀土掘金技術社區,作者Sawtone係一位前端新手。佢喺學習Next.js嘅時候接觸到shadcn/ui,發現呢個被稱為「組件庫」嘅工具,其實完全唔係傳統意義上嗰種。作者想講清楚shadcn/ui嘅設計哲學:佢唔係一個包埋一齊嘅組件庫,而係一個用嚟構建你自己組件庫嘅工具集。
文章詳細解釋咗shadcn/ui嘅核心:基於組合嘅統一API。佢哋用Radix UI做底層,統一曬所有組件嘅行為同狀態管理;再用Tailwind CSS加上cva(Class Variance Authority)同tailwind-merge,實現樣式API嘅統一。開發者唔使記住一大堆新Prop,只要按照React最常見嘅模式嚟組合就得。
作者仲提供咗三個實戰示例:內部統一(用Command同Dialog組合命令面板)、外部統一第三方(封裝Recharts圖表成符合shadcn風格嘅Card容器)、外部統一自建(做一個有顯示/隱藏功能嘅密碼輸入框)。最後強調shadcn/ui嘅三大好處:源碼即文檔(可以白盒追查問題)、輕鬆定製(直接改源文件,唔使Hack CSS)、AI友好(結構清晰,方便AI生成代碼)。仲有佢嘅代碼分發系統,用規約同CLI管理組件,好似npm但要科學。
- 結論:shadcn/ui唔係組件庫,而係構建組件庫嘅工具集,開發者擁有完全控制權。
- 方法:透過Radix UI提供無樣式組件原語,再用Tailwind CSS加cva統一管理樣式變體,實現真正組合式開發。
- 差異:傳統庫用固定Prop接口如variant, color,shadcn/ui嘅統一API係基於組合模式同社區最佳實踐,開發者唔使學新嘢。
- 啟發:源碼即文檔,可以直接睇源文件揾問題,仲可以任意修改組件,例如加framer-motion動畫都唔使Hack CSS。
- 可行動點:使用shadcn CLI同規約管理自建組件,導入時自動檢查依賴,實現輕鬆代碼分發。
唔係組件庫,而係工具集
shadcn/ui呢個名聽落似組件庫,但佢哋官方自己都話:我哋唔係組件庫,而係一個代碼分發平台,係你建立自己組件庫嘅方法論同工具集。咁樣嘅定位同傳統嘅 Material-UI 或者 Ant Design 好唔同。
唔係傳統組件庫
傳統庫會將所有組件打包成npm包,開發者用嘅時候只能黑盒使用,想改樣式就好易要用 !important 或者覆蓋CSS。但shadcn/ui直接將源碼複製到你個項目入面,你完全控制,想點改就點改,唔使受限制。
統一API:Radix + Tailwind + cva
shadcn/ui嘅統一API分兩個層面:行為狀態同樣式。行為狀態方面,底層用咗 Radix UI,一個Headless UI庫,將組件邏輯同樣式完全分離。例如Dialog會拆成Root、Trigger、Content、Close等部分,開發者可以自由組合。
Radix UI
狀態管理都係統一模式:可以畀組件自我管理(非可控),又或者用 `open`、`onOpenChange` 等Prop嚟精確控制,所有需要開關狀態嘅組件都係咁樣。
統一模式:open / onOpenChange
樣式方面就靠 Tailwind CSS配合cva(Class Variance Authority)同tailwind-merge。cva以結構化方式定義樣式變體,例如Button嘅 `variant`(default, destructive, outline)同 `size`(default, sm, lg),然後根據傳入Props生成對應Tailwind類名。tailwind-merge就負責解決類名衝突,確保最終風格正確。
cva
tailwind-merge
呢種做法令到所有組件嘅樣式API都一致——只要改className就得。你寫嘅自定義組件都可以套用呢個模式。
實戰示例:命令面板、圖表容器、密碼輸入
作者提供咗三個例子嚟展示shadcn/ui嘅組合能力同統一性。第一個係內部統一:用 `Dialog` 同 `Command` 組合一個似macOS Spotlight嘅命令面板。關鍵係 `asChild` 屬性,佢令到任何組件都可以變成觸發器,唔一定要用原身嘅button。
asChild
第二個係外部統一(第三方):將圖表庫 Recharts 封裝成一個 `ChartContainer` 組件,用Card做外殼,統一標題、描述同邊框。開發者就唔使每次都寫一堆模板代碼,而且如果日後轉圖表庫,只改一個組件就得。
封裝與適配器模式
第三個係自建組件:做一個密碼輸入框,有顯示/隱藏切換功能。呢度示範咗「屬性透傳」,將所有原生Input嘅Props(placeholder、value、onChange等)直接透過 `...props` 傳落去,使用者可以當普通Input咁用。
屬性透傳
- 用 `asChild` 將任何組件變成觸發器,組合性強
- 封裝第三方庫,統一UI體驗,低耦合高維護性
- 屬性透傳令你嘅組件同原生Input一樣易用
高度可定製:源碼即文檔、輕鬆改動、AI-Friendly
因為shadcn/ui將源碼放喺你項目入面,所以你可以直接修改任何組件。例如你想為AlertDialog加入framer-motion動畫,就喺源文件加個 `motion.div` 包住個Content,完全唔使Hack CSS或者用 `!important`。
直接修改源碼
作者仲提到AI-Friendly:因為shadcn結構清晰(基於cva),你只需要同AI講「呢個係shadcn/ui組件,我需要XX」,佢就好易生成正確代碼。
AI-Friendly
另外,源碼即文檔嘅好處仲有:你可以直接跳去定義睇底層Radix點樣處理事件,唔使查文獻、上Issues。作者話呢啲係「從黑盒猜測變為白盒溯源」。
白盒溯源
代碼分發:規約同CLI
shadcn/ui仲有一套代碼分發系統,由規約(convention)同CLI組成。規約係一個扁平化文件結構,定義組件名稱、依賴同檔案路徑。你可以為自建組件寫Schema,好似 `components.json` 入面加 `password-input` 嘅規約。
規約
CLI指令 `shadcn-ui add` 唔係就咁npm install,佢會運行安裝腳本,將源碼複製到 `components` 資料夾,同時檢查並安裝所有依賴(包括其他shadcn組件同npm套件)。咁樣就可以好似npm一樣科學管理代碼,但係你有曬源碼控制權。
CLI
{
"tsx": {
"$schema": "...",
"components": {
"password-input": {
"name": "PasswordInput",
"dependencies": ["input", "button"],
"registryDependencies": ["lucide-react"],
"files": ["components/ui/password-input.tsx"]
}
}
}
}
呢套系統令到團隊可以一致咁管理自建組件,而且透過CLI安裝時,佢會自動處理依賴,唔怕漏咗嘢。
https://juejin.cn/post/7534717654966386739
前言
最近打算由頭學 Next.js,試嚇用佢嘅 SSG 支援嚟整自己嘅新網站。講到 Next.js,就不得不提埋 shadcn/ui 呢個 next 嘅好拍檔
咦,官網睇落好高級咁,咁呢篇文章我哋就一齊學嚇呢個 唔稱自己做組件庫嘅「組件庫」
正文
先譯一譯上面啲字:
shadcn/ui 係一套設計靚、高可訪問性嘅組件,同時又係一個代碼分發平台。佢可以同你鍾意嘅框架同AI模型一齊運作。佢秉持開源精神,並且開放組件原始碼。
佢唔係一個傳統嘅組件庫,而係你用嚟建立自己組件庫嘅方法論同工具集。
咁傳統組件庫同「扮清高」嘅 shadcn 有乜嘢分別呢?
基於組合嘅統一 API
shadcn/ui 裏面每個組件都共享一個統一、基於組合嘅接口模式,無論係官方組件、第三方組件還是自己開發嘅新組件都可以保持一致同協作,開發者唔使學任何新組件嘅 API
呢點同 Material-UI 或者 Ant Design 呢啲傳統組件庫有啲唔同,傳統意義嘅「統一 API」通常指庫嘅作者為所有組件定義咗一套共享式統一嘅 Prop 接口,例如 variant, color, size 等
而 shadcn/ui 嘅「統一 API」並唔係指一套固定嘅 Prop 集合,而係一種統一、基於組合嘅構建模式同交互慣例,所有組件都跟從 React 同前端社區已經廣泛接受嘅最佳實踐。
深入啲講,呢種新型「統一性」嘅特點主要體現喺兩個方面:
⬤ 行為同狀態嘅統一:Radix UI
shadcn/ui 嘅絕大多數組件響底層都係基於 Radix UI 呢個 Headless UI 庫[1],Radix 提供咗無樣式、功能齊全、高可訪問性嘅組件原語。
[1] Headless UI 庫係一種前端開發模式,核心係將組件嘅邏輯同樣式分開。呢種開發模式令開發者可以喺保持組件功能嘅同時,完全控制組件嘅外觀同風格,唔受特定 UI 框架限制。
喺行為同狀態方面,shadcn/ui 跟從關注點分離,用 Radix 將組件嘅行為、狀態管理同視覺表現完全解耦。
shadcn/ui 只負責視覺層,而 Radix 只負責核心邏輯,從而確保所有官方組件喺「骨架」層面完全統一。
詳細啲講,行為上Radix 組件被拆解成多個邏輯部分,例如 Dialog.Root, Dialog.Trigger, Dialog.Content, Dialog.Close,無論開發乜嘢具體組件,呢啲結構都係可重用嘅,開發者透過組合呢啲部分嚟構建完整嘅組件。呢種構建模式係固定、可預測嘅。
而狀態方面,Radix 為狀態管理提供咗統一模式,開發者可以俾組件自己管理狀態(非可控),亦可以透過 open, onOpenChange 呢啲 props 嚟精確控制狀態(可控)。呢種模式喺所有需要管理開關狀態嘅組件中保持一致。
⬤ 樣式定製嘅統一:Tailwind CSS + cva
喺樣式上,shadcn/ui 用 Tailwind CSS 嚟做樣式定義,並透過 cva (Class Variance Authority) 和 tailwind-merge 嚟管理樣式嘅變體同合併,從而達成樣式 API 嘅統一。
講到 Tailwind,就不得不提 原子化 CSS 理念喇,shadcn 透過 Tailwind 提供咗一套功能性嘅 CSS 類名,開發者只需透過 className 呢個 React 最原生嘅 Prop 嚟為所有組件應用樣式, className 變成一個直通屬性。
除此之外,shadcn 仲有效藉助咗 cva,佢容許你以結構化方式建立組件嘅樣式變體。例如,一個掣可以有 variant(default, destructive, outline) 和 size(default, sm, lg) 嘅變體。cva 會產生一個函數,呢個函數根據你傳入嘅 props (variant, size) 返回對應嘅 Tailwind 類名字串。呢種模式可以應用喺任何自訂或者第三方組件上,從而實現樣式 API 嘅統一。
呢陣時你可能會疑惑:咦,咁出現衝突點算?唔使驚,我哋仲有 tailwind-merge,喺組合 cva 嘅變體類名同用戶傳入嘅 className 時,tailwind-merge 能夠智能咁解決衝突,確保最終應用嘅樣式符合預期。
示例
1. 內部統一:構建仿 macOS Spotlight 嘅命令面板 (⌘+K)
import { Dialog, DialogContent, DialogTrigger, } from "@/components/ui/dialog"
import { Command, CommandInput, CommandList, CommandItem, CommandEmpty, } from "@/components/ui/command"
import { Button } from "@/components/ui/button"
// Command + Dialog = CommandPalette,that's why 組合優於繼承
export function CommandPalette() {
return (
<Dialog>
<DialogTrigger asChild>
{/* `asChild` 讓任何組件都能成為觸發器 */}
<Button variant="outline">打開命令面板 (⌘+K)</Button>
</DialogTrigger>
<DialogContent className="p-0">
<Command>
<CommandInput placeholder="輸入命令或搜索..." />
<CommandList>
<CommandEmpty>未找到結果.</CommandEmpty>
<CommandItem>個人設置</CommandItem>
<CommandItem>賬單</CommandItem>
</CommandList>
</Command>
</DialogContent>
</Dialog>
)
}
註釋嘅地方比較關鍵,我哋一齊嚟睇嚇:
不使用 asChild:如果我哋直接寫<DialogTrigger>打開</DialogTrigger>,佢會自己渲染成一個默認嘅,樣式簡單嘅<button>元素使用 asChild:asChild屬性話俾DialogTrigger:“唔好自己渲染成一個掣,而係將你嘅所有功能(例如onClick事件處理器嚟打開對話框)附加到你嘅直接子組件度”。
呢度嘅子組件係 <Button variant="outline">。所以,最終渲染到頁面上嘅結果就係:呢個 <Button> 組件獲得咗「點擊後打開對話框」嘅能力。
咁樣做有乜好處?
組合性: 開發者可以將任何可互動嘅組件(自訂掣、圖標、一段文字等)作為 onClick等等嘅觸發器,而唔使改變 Radix 原語Dialog嘅邏輯。樣式控制: 開發者可以完全自由咁定製觸發器嘅樣式。呢度我哋用咗 <Button>組件自帶嘅variant="outline"樣式,非常方便。
2. 外部統一(第三方):為圖表庫 Recharts 封裝帶標題同邊框嘅容器
import { BarChart, Bar, XAxis, YAxis } from'recharts'; // 假設這是第三方庫
import { Card, CardContent, CardHeader, CardTitle, CardDescription} from"@/components/ui/card"
// 首先創建一個統一的圖表容器,它的 API 是 shadcn 風格的
exportfunction ChartContainer({ title, description, chartData, className }) {
return (
<Card className={className}> // 統一接收 className
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>
{/* 無縫置入第三方組件,屬性已簡化,實際場景可通過 Props 傳入 */}
<BarChart data={chartData} width={400} height={200}>
<XAxis dataKey="name" />
<YAxis />
<Bar dataKey="value" fill="#8884d8" />
</BarChart>
</CardContent>
</Card>
)
}
// 使用封裝後的組件
const myData = [{ name: 'A', value: 400 }, { name: 'B', value: 300 }];
<ChartContainer
title="月度收入"
description="最近六個月的收入趨勢"
chartData={myData}
/>
呢段樣例展示咗設計模式中嘅 封裝與適配器模式,將一個第三方庫(recharts)嘅 API 同樣式,封裝成符合我哋自己項目設計規範嘅組件。
<BarChart data={chartData} ...> 體現咗適配器模式嘅核心——轉換接口。
最終,我哋達成咗以下優勢:
易使用: 如果唔封裝,開發者每次都要寫一次 Card,CardHeader,BarChart... 嘅所有模板代碼。而家只需要調用ChartContainer並傳入三個清晰嘅屬性。視覺一致 : 所有嘅圖表都被 Card包裹,並有統一嘅Header樣式,用戶體驗++。高可維護性 & 低耦合 : 如果將來團隊決定唔再用 recharts,想換一個性能更好嘅新圖表庫,只需要換曬每一個<ChartContainer />,而唔使手動改曬所有BarChart嘅 API。使用者唔需要知道封裝嘅實現細節: 使用者只需要「聲明」你想要一個標題係「月度收入」嘅圖表,數據係 myData,而唔使關心佢內部點樣用Card和BarChart實現。
3. 外部統一(自建):建立一個有「顯示/隱藏」切換功能嘅密碼輸入框
import React from"react"
import { Input } from"@/components/ui/input"
import { Button } from"@/components/ui/button"
import { Eye, EyeOff } from"lucide-react"
import { cn } from"@/lib/utils"
exportfunction PasswordInput({ className, ...props }) {
const [show, setShow] = React.useState(false)
return (
<div className={cn("relative", className)}>
{/* 使用 屬性透傳 傳遞所有 Input 的原生 props */}
<Input type={show ? "text" : "password"} {...props} />
{/* 使用 Button 的標準變體和尺寸 */}
<Button
type="button" // 避免觸發表單提交
variant="ghost"
size="icon"
className="absolute top-1/2 right-2 -translate-y-1/2"
onClick={() => setShow(!show)}
>
{show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
)
}
// 使用起來就像一個原生組件口牙
<PasswordInput placeholder="請輸入密碼" />
都係先睇註釋部分,透過註釋處嘅 屬性透傳,PasswordInput 繼承了 <Input> 嘅差唔多所有能力。你可以好似用普通 <Input> 噉樣俾佢傳 placeholder, id, name, value, onChange, onFocus, disabled 等等,唔使喺 PasswordInput 組件內部對呢啲屬性做任何額外處理。
掣相關呢啲都係基礎操作,但都唔妨礙我哋感受 shadcn/ui 嘅靚。
type="button"覆蓋了 <form> 標籤內掣嘅默認 type "submit",防止意外觸發表單
variant="ghost" 和 size="icon"是shadcn/ui嘅標準化 API,簡單實用就可以確保視覺效果靚,唔使做額外 CSS Hack,老細再唔使擔心我嘅 CSS 樣式 bug && bug 啦。
高度可定製
shadcn/ui 將實際嘅組件代碼交到開發者手上,開發者對代碼有完全嘅控制權,可以根據自己嘅
1. 源碼即文檔
用傳統組件庫開發遇到問題時,我哋通常要查文檔、搜 issue、搲爆頭(劃掉),但喺 shadcn/ui 入面,我哋可以透過原始碼得到解決問題所需嘅大量資訊,將 「黑盒猜測 + 排除」 變為了 「白盒溯源」 。
示例
eg:咦,Dialog 組件點擊頁面某個特定區域時點解冇好似預期咁關閉?
唔使查文檔 prop,(暫時)唔使調試,(暫時)唔使 issue,JUST Go to defination
// components/ui/dialog.tsx
<AlertDialogPrimitive.Content
ref={ref}
// ... 其他 props
onPointerDownOutside={(event) => {
// 由底層 Radix UI 控制
// 如果外部元素調用了 event.preventDefault(),這裏的邏輯就不會觸發
console.log("外部點擊事件源: ", event.target);
}}
// ...
/>
結論:哦~ 問題好可能係我點擊嗰個區域執行了 event.preventDefault()
2. 輕鬆定製組件
好消息!對組件嘅個性化定製再唔使 強行覆蓋 CSS 或者 !important 啓動! 了
示例
eg:為 AlertDialog 注入 framer-motion 動畫,呢個 prop 揾唔到,CSS keyframes 同選擇器覆蓋樣式衝突 都唔得
// 文件: components/ui/alert-dialog.tsx (修改後)
import { motion } from "framer-motion";
// ... 其他 import
const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPortal>
{/* ... */}
{/* 將 AlertDialogPrimitive.Content 用 motion.div 包裹起來 */}
<motion.div
initial={{ opacity: 0, y: -50, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ type: "spring", stiffness: 260, damping: 20 }}
>
<AlertDialogPrimitive.Content ref={ref} className={cn(/*...*/, className)} {...props} />
</motion.div>
</AlertDialogPortal>
))
個性化修改組件庫入面嘅組件,其實更加係對組件嘅一種增強,呢個正係 shadcn/ui 所講嘅:我哋唔係組件庫。而係構建組件庫嘅工具集。
3. AI-Friendly
告別複雜提示詞,just say:呢個係基於 cva 嘅 shadcn/ui 組件,我需要xx
代碼分發
shadcn/ui 亦係一個代碼分發系統。佢為組件定義咗一套規約,並提供咗一個命令行界面(CLI)嚟分發佢哋。
⬤ 規約:同包管理一樣科學易用
規約 係一種扁平化嘅文件結構,用嚟定義組件、佢哋嘅依賴項同屬性。用規約添加同管理組件又方便又科學。
eg:為自建嘅 PasswordInput 建立 Schema
// components.json (部分)
{
"tsx": {
"$schema": "...",
"components": {
// ... 官方組件
// 為自建組件設置規約
"password-input": {
"name": "PasswordInput",
// 依賴於項目內已有的 shadcn 組件
"dependencies": ["input", "button"],
// 依賴於需要從 npm 安裝的包
"registryDependencies": ["lucide-react"],
// 組件的源文件路徑
"files": ["components/ui/password-input.tsx"]
}
}
}
}
⬤ 命令行界面 (CLI):組件嚟到我身邊
CLI 唔係簡單嘅 npm install,npm install 安裝嘅係一個黑盒包,而 shadcn-ui add 運行嘅係一個安裝腳本。
CLI 將源代碼複製到你項目嘅 components 文件夾,並檢查、安裝所有依賴。
後記
寫到呢度差唔多就完喇,其實仲有好多可以深入嘅嘢可以寫,反應好嘅話再出嘿嘿👉👈
咦,可能有同學會問:老師老師我唔識寫 TypeScript 係咪冇得用呢個嘢呀?
有嘅兄弟有嘅,往期回顧:
[前端] Leader:可以唔用但要知道😠一文速查 TypeScript 基礎知識點,字典式速查,全文乾貨! - 掘金[1]
到呢度就真係完喇,我係 Sawtone,前端新手一個,祝你開心。
相關官方網站
Next.js --> www.nextjs.cn[2]
shadcn/ui --> www.shadcn-ui.cn[3]
Tailwind Css --> www.tailwindcss.cn[4]
Radix UI --> radix.zhcndoc.com[5]
https://juejin.cn/post/7534717654966386739
前言
最近打算從頭學習 Next.js,嘗試憑藉其卓越的 SSG 支持來構建自己的新網站,那提到 Next.js,不可避免地還要說說 shadcn/ui 這個 next 的好夥伴
誒,官網看起來很高級的樣子,那這篇文章我們就來一起學學這個 不稱自己為組件庫的“組件庫”
正文
先來翻譯一下上面的文字:
shadcn/ui 是一套設計精美、高可訪問性的組件,同時也是一個代碼分發平台。它可與您喜愛的框架和AI模型協同工作。它秉持開源精神,並開放組件源代碼。
它並非一個傳統的組件庫,而是您用以構建自己組件庫的方法論與工具集。
那麼,傳統組件庫與 “自命清高” 的 shadcn 有什麼區別呢?
基於組合的統一 API
shadcn/ui 中每個組件都共享一個統一的、基於組合的接口模式,無論是官方組件、第三方組件還是自己開發的新組件都能保持匹配與協同,開發者無需學習任何新組件的API
這與 Material-UI 或者 Ant Design 等傳統組件庫還略有不同,傳統意義的“統一 API”通常意味着庫的作者為所有組件定義了一套共享式統一的 Prop 接口,例如 variant, color, size 等
而 shadcn/ui 的“統一 API”並非指一套固定的 Prop 集合,而是一種統一的、基於組合的構建模式和交互慣例,所有組件都遵循 React 與前端社區已廣泛接受的最佳實踐。
深入來說,這種新型“統一性”的特點主要體現在兩個方面:
⬤ 行為與狀態的統一:Radix UI
shadcn/ui 的絕大多數組件在底層都基於 Radix UI 這個 Headless UI 庫[1],Radix 提供了無樣式、功能完備、高度可訪問的組件原語。
[1] Headless UI 庫是一種前端開發模式,其核心在於將組件的邏輯和樣式分離。 這種開發模式允許開發者在保持組件功能性的同時,完全控制組件的外觀和風格,而不受特定 UI 框架的限制。
在行為與狀態上,shadcn/ui 遵循關注點分離,使用 Radix 將組件的行為、狀態管理與視覺表現完全解耦。
shadcn/ui 僅負責視覺層,而 Radix 僅負責核心邏輯,從而確保所有官方組件在“骨架”層面是完全統一的。
詳細來說,行為上,Radix 組件被拆解為多個邏輯部分,例如 Dialog.Root, Dialog.Trigger, Dialog.Content, Dialog.Close,無論開發什麼具體組件,這些結構都是可複用的,開發者通過組合這些部分來構建完整的組件。這種構建模式是固定的、可預測的。
而狀態上,Radix 為狀態管理提供了統一模式,開發者可以讓組件自我管理狀態(非可控),也可以通過 open, onOpenChange 等 props 來精確控制狀態(可控)。這種模式在所有需要管理開關狀態的組件中保持一致。
⬤ 樣式定製的統一:Tailwind CSS + cva
在樣式上,shadcn/ui 採用 Tailwind CSS 進行樣式定義,並通過 cva (Class Variance Authority) 和 tailwind-merge 來管理樣式的變體和合並,從而達成樣式 API 的統一。
提到 Tailwind,那就不得不提到 原子化 CSS 理念了,shadcn 通過 Tailwind 提供了一套功能性的 CSS 類名,開發者僅需通過 className 這個 React 最原生的 Prop 來為所有組件應用樣式, className 變為一個直通屬性。
除此之外,shadcn 還有效藉助了 cva,它允許你以結構化的方式創建組件的樣式變體。例如,一個按鈕可以有 variant(default, destructive, outline) 和 size(default, sm, lg) 的變體。cva 會生成一個函數,這個函數根據你傳入的 props (variant, size) 返回對應的 Tailwind 類名字符串。這種模式可以被應用到任何自定義或第三方組件上,從而實現樣式 API 的統一。
這時你可能會疑惑:誒那出現衝突怎麼辦呢?沒關係,我們還有 tailwind-merge,在組合 cva 的變體類名和用戶傳入的 className 時,tailwind-merge 能夠智能地解決衝突,確保最終應用的樣式符合預期。
示例
1. 內部統一:構建仿 macOS Spotlight 的命令面板 (⌘+K)
import { Dialog, DialogContent, DialogTrigger, } from "@/components/ui/dialog"
import { Command, CommandInput, CommandList, CommandItem, CommandEmpty, } from "@/components/ui/command"
import { Button } from "@/components/ui/button"
// Command + Dialog = CommandPalette,that's why 組合優於繼承
export function CommandPalette() {
return (
<Dialog>
<DialogTrigger asChild>
{/* `asChild` 讓任何組件都能成為觸發器 */}
<Button variant="outline">打開命令面板 (⌘+K)</Button>
</DialogTrigger>
<DialogContent className="p-0">
<Command>
<CommandInput placeholder="輸入命令或搜索..." />
<CommandList>
<CommandEmpty>未找到結果.</CommandEmpty>
<CommandItem>個人設置</CommandItem>
<CommandItem>賬單</CommandItem>
</CommandList>
</Command>
</DialogContent>
</Dialog>
)
}
註釋的地方比較關鍵,我們一起來看看:
不使用 asChild:如果我們直接寫<DialogTrigger>打開</DialogTrigger>,它會自己渲染成一個默認的,樣式簡單的<button>元素使用 asChild:asChild屬性告訴DialogTrigger:“不要自己渲染成一個按鈕,而是把你所有的功能(比如onClick事件處理器來打開對話框)附加到你的直接子組件上”。
這裏的子組件是 <Button variant="outline">。因此,最終渲染到頁面上的結果就是:這個 <Button> 組件獲得了“點擊後打開對話框”的能力。
這樣做的好處是什麼?
組合性: 開發者可以把任何可交互的組件(自定義按鈕、圖標、一段文字等)作為 onClick等觸發器,而無需改變 Radix 原語Dialog的邏輯。樣式控制: 開發者可以完全自由地定製觸發器的樣式。這裏我們使用了 <Button>組件自帶的variant="outline"樣式,非常方便。
2. 外部統一(第三方):為圖表庫 Recharts 封裝帶標題和邊框的容器
import { BarChart, Bar, XAxis, YAxis } from'recharts'; // 假設這是第三方庫
import { Card, CardContent, CardHeader, CardTitle, CardDescription} from"@/components/ui/card"
// 首先創建一個統一的圖表容器,它的 API 是 shadcn 風格的
exportfunction ChartContainer({ title, description, chartData, className }) {
return (
<Card className={className}> // 統一接收 className
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>
{/* 無縫置入第三方組件,屬性已簡化,實際場景可通過 Props 傳入 */}
<BarChart data={chartData} width={400} height={200}>
<XAxis dataKey="name" />
<YAxis />
<Bar dataKey="value" fill="#8884d8" />
</BarChart>
</CardContent>
</Card>
)
}
// 使用封裝後的組件
const myData = [{ name: 'A', value: 400 }, { name: 'B', value: 300 }];
<ChartContainer
title="月度收入"
description="最近六個月的收入趨勢"
chartData={myData}
/>
這段樣例展示了設計模式中的 封裝與適配器模式,將一個第三方庫(recharts)的 API 和樣式,封裝成了符合我們自己項目設計規範的組件。
<BarChart data={chartData} ...> 體現了適配器模式的核心——轉換接口。
最終,我們達成了以下優勢:
易使用: 如果不封裝,開發者需要每次都寫一遍 Card,CardHeader,BarChart... 的所有模板代碼。而現在只需要調用ChartContainer並傳入三個清晰明瞭的屬性。視覺一致 : 所有的圖表都被 Card包裹,並擁有統一的Header樣式,用戶體驗++。高可維護性 & 低耦合 : 如果未來團隊決定不再使用 recharts,而是想換成一個性能更好的新圖表庫,只需要替換每一個<ChartContainer />,而不用手動更換所有BarChart的 API。使用者無需得知封裝實現細節: 使用者只需要“聲明”你想要一個標題為“月度收入”的圖表,數據是 myData,而無需關心它內部是如何用Card和BarChart實現的。
3. 外部統一(自建):創建一個帶“顯示/隱藏”切換功能的密碼輸入框
import React from"react"
import { Input } from"@/components/ui/input"
import { Button } from"@/components/ui/button"
import { Eye, EyeOff } from"lucide-react"
import { cn } from"@/lib/utils"
exportfunction PasswordInput({ className, ...props }) {
const [show, setShow] = React.useState(false)
return (
<div className={cn("relative", className)}>
{/* 使用 屬性透傳 傳遞所有 Input 的原生 props */}
<Input type={show ? "text" : "password"} {...props} />
{/* 使用 Button 的標準變體和尺寸 */}
<Button
type="button" // 避免觸發表單提交
variant="ghost"
size="icon"
className="absolute top-1/2 right-2 -translate-y-1/2"
onClick={() => setShow(!show)}
>
{show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
)
}
// 使用起來就像一個原生組件口牙
<PasswordInput placeholder="請輸入密碼" />
還是先看註釋部分,通過註釋處的 屬性透傳,PasswordInput 繼承了 <Input> 的幾乎所有能力。你可以像使用普通 <Input> 一樣給它傳遞 placeholder, id, name, value, onChange, onFocus, disabled 等等,無需在 PasswordInput 組件內部對這些屬性做任何額外的處理。
按鈕相關這都是一些基礎操作,但也不妨礙我們感受 shadcn/ui 的美。
type="button"覆蓋了 <form> 標籤內按鈕的默認 type "submit",防止意外觸發表單
variant="ghost" 和 size="icon"是shadcn/ui的標準化 API,簡單實用就能確保視覺效果美觀,不用進行額外 CSS Hack,leader 再也不用擔心我的 Css 樣式 bug && bug 啦。
高度可定製
shadcn/ui 將實際的組件代碼交到開發者手中,開發者對代碼擁有完全的控制權,可以根據自己的
1. 源碼即文檔
使用傳統組件庫開發遇到問題時,我們往往需要查閲文檔、搜索 issue、抓耳撓腮(劃掉),但在 shadcn/ui 中,我們可以通過源代碼獲得解決問題所需的龐大信息,把 “黑盒猜測 + 排除” 變為了 “白盒溯源” 。
示例
eg:誒,Dialog 組件在點擊頁面某個特定區域時怎麼沒像預期一樣關閉?
無需查閲文檔 prop,(暫時)無需調試,(暫時)無需 issue,JUST Go to defination
// components/ui/dialog.tsx
<AlertDialogPrimitive.Content
ref={ref}
// ... 其他 props
onPointerDownOutside={(event) => {
// 由底層 Radix UI 控制
// 如果外部元素調用了 event.preventDefault(),這裏的邏輯就不會觸發
console.log("外部點擊事件源: ", event.target);
}}
// ...
/>
結論:哦~ 問題很可能是我點擊的那個區域執行了 event.preventDefault()
2. 輕鬆定製組件
喜報!對組件的個性化定製再也不用 強行覆蓋 CSS 或者 !important 啓動! 了
示例
eg:為 AlertDialog 注入 framer-motion 動畫,這 prop 找不到,CSS keyframes 和選擇器覆蓋樣式衝突 都不行啊
// 文件: components/ui/alert-dialog.tsx (修改後)
import { motion } from "framer-motion";
// ... 其他 import
const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPortal>
{/* ... */}
{/* 將 AlertDialogPrimitive.Content 用 motion.div 包裹起來 */}
<motion.div
initial={{ opacity: 0, y: -50, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ type: "spring", stiffness: 260, damping: 20 }}
>
<AlertDialogPrimitive.Content ref={ref} className={cn(/*...*/, className)} {...props} />
</motion.div>
</AlertDialogPortal>
))
個性化修改組件庫內的組件,實則更是對組件的一種增強,這正是 shadcn/ui 所說的:我們不是組件庫。而是構建組件庫的工具集。
3. AI-Friendly
告別複雜提示詞,just say:這是一個基於 cva 的 shadcn/ui 組件,我需要xx
代碼分發
shadcn/ui 也是一個代碼分發系統。它為組件定義了一套規約,並提供了一個命令行界面(CLI)來分發它們。
⬤ 規約:與包管理一樣科學易用
規約 是一種扁平化的文件結構,用以定義組件、它們的依賴項和屬性。使用規約添加與管理組件即方便又科學。
eg:為自建的 PasswordInput 創建 Schema
// components.json (部分)
{
"tsx": {
"$schema": "...",
"components": {
// ... 官方組件
// 為自建組件設置規約
"password-input": {
"name": "PasswordInput",
// 依賴於項目內已有的 shadcn 組件
"dependencies": ["input", "button"],
// 依賴於需要從 npm 安裝的包
"registryDependencies": ["lucide-react"],
// 組件的源文件路徑
"files": ["components/ui/password-input.tsx"]
}
}
}
}
⬤ 命令行界面 (CLI):組件來我身邊
CLI 不是簡單的 npm install,npm install 安裝的是一個黑盒包,而 shadcn-ui add 運行的是一個安裝腳本。
CLI 將源代碼複製到你項目的 components 文件夾,並檢查、安裝所有依賴。
後記
寫到這裏差不多就結束啦,其實還有很多能夠深入的東西可以寫,反響好的話再出嘿嘿👉👈
誒,可能有同學會問:老師老師我不會寫 TypeScript 是不是沒法用這個東西呀?
有的兄弟有的,往期回顧:
[前端] Leader:可以不用但要知道😠一文速查 TypeScript 基礎知識點,字典式速查,全文乾貨! - 掘金[1]
到這裏就真的結束啦,我是 Sawtone,前端新手一枚,祝你開心。
相關官方網站
Next.js --> www.nextjs.cn[2]
shadcn/ui --> www.shadcn-ui.cn[3]
Tailwind Css --> www.tailwindcss.cn[4]
Radix UI --> radix.zhcndoc.com[5]