圖文 + 多個場景案例詳解 shadcn + tailwind 顛覆性組件開發

作者:前端自習課
日期:2025年10月31日 上午10:25
來源:WeChat 原文

整理版優先睇

速讀 5 個重點 高亮

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最常見嘅模式嚟組合就得。

作者仲提供咗三個實戰示例:內部統一(用CommandDialog組合命令面板)、外部統一第三方(封裝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做外殼,統一標題、描述同邊框。開發者就唔使每次都寫一堆模板代碼,而且如果日後轉圖表庫,只改一個組件就得。

封裝與適配器模式

第三個係自建組件:做一個密碼輸入框,有顯示/隱藏切換功能。呢度示範咗「屬性透傳」,將所有原生InputProps(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

自建PasswordInput規約示例(components.json部分) json
{
    "tsx": {
        "$schema": "...",
        "components": {
            "password-input": {
                "name": "PasswordInput",
                "dependencies": ["input", "button"],
                "registryDependencies": ["lucide-react"],
                "files": ["components/ui/password-input.tsx"]
            }
        }
    }
}

呢套系統令到團隊可以一致咁管理自建組件,而且透過CLI安裝時,佢會自動處理依賴,唔怕漏咗嘢。


呢篇文章轉載自稀土掘金技術社區,作者:Sawtone

https://juejin.cn/post/7534717654966386739


前言

最近打算由頭學 Next.js,試嚇用佢嘅 SSG 支援嚟整自己嘅新網站。講到 Next.js,就不得不提埋 shadcn/ui 呢個 next 嘅好拍檔

咦,官網睇落好高級咁,咁呢篇文章我哋就一齊學嚇呢個 唔稱自己做組件庫嘅「組件庫」


正文

先譯一譯上面啲字:

shadcn/ui 係一套設計靚、高可訪問性嘅組件,同時又係一個代碼分發平台。佢可以同你鍾意嘅框架同AI模型一齊運作。佢秉持開源精神,並且開放組件原始碼。

佢唔係一個傳統嘅組件庫,而係你用嚟建立自己組件庫嘅方法論同工具集。

咁傳統組件庫同「扮清高」嘅 shadcn 有乜嘢分別呢?


sha2.png

基於組合嘅統一 API

shadcn/ui 裏面每個組件都共享一個統一、基於組合嘅接口模式,無論係官方組件第三方組件還是自己開發嘅新組件都可以保持一致同協作,開發者唔使學任何新組件嘅 API

呢點同 Material-UI 或者 Ant Design 呢啲傳統組件庫有啲唔同,傳統意義嘅「統一 API」通常指庫嘅作者為所有組件定義咗一套共享式統一嘅 Prop 接口,例如 variantcolorsize 等

而 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.RootDialog.TriggerDialog.ContentDialog.Close,無論開發乜嘢具體組件,呢啲結構都係可重用嘅,開發者透過組合呢啲部分嚟構建完整嘅組件。呢種構建模式係固定、可預測嘅。

而狀態方面,Radix 為狀態管理提供咗統一模式,開發者可以俾組件自己管理狀態(非可控),亦可以透過 openonOpenChange 呢啲 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 (variantsize) 返回對應嘅 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> 元素
  • 使用 asChildasChild 屬性話俾 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'value400 }, { name'B'value300 }];
<ChartContainer
  title="月度收入"
  description="最近六個月的收入趨勢"
  chartData={myData}
/>

呢段樣例展示咗設計模式中嘅 封裝與適配器模式,將一個第三方庫(recharts)嘅 API 同樣式,封裝成符合我哋自己項目設計規範嘅組件。

<BarChart data={chartData} ...> 體現咗適配器模式嘅核心——轉換接口

最終,我哋達成咗以下優勢:

  • 易使用: 如果唔封裝,開發者每次都要寫一次 CardCardHeaderBarChart... 嘅所有模板代碼。而家只需要調用 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> 噉樣俾佢傳 placeholderidnamevalueonChangeonFocusdisabled 等等,唔使喺 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: 0y: -50scale: 0.9 }}
      animate={{ opacity: 1y: 0scale: 1 }}
      transition={{ type: "spring", stiffness: 260damping: 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 installnpm 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]


本文轉載於稀土掘金技術社區,作者:Sawtone

https://juejin.cn/post/7534717654966386739


前言

最近打算從頭學習 Next.js,嘗試憑藉其卓越的 SSG 支持來構建自己的新網站,那提到 Next.js,不可避免地還要說說 shadcn/ui 這個 next 的好夥伴

誒,官網看起來很高級的樣子,那這篇文章我們就來一起學學這個 不稱自己為組件庫的“組件庫”


正文

先來翻譯一下上面的文字:

shadcn/ui 是一套設計精美、高可訪問性的組件,同時也是一個代碼分發平台。它可與您喜愛的框架和AI模型協同工作。它秉持開源精神,並開放組件源代碼。

它並非一個傳統的組件庫,而是您用以構建自己組件庫的方法論與工具集。

那麼,傳統組件庫與 “自命清高” 的 shadcn 有什麼區別呢?


sha2.png

基於組合的統一 API

shadcn/ui 中每個組件都共享一個統一的、基於組合的接口模式,無論是官方組件第三方組件還是自己開發的新組件都能保持匹配與協同,開發者無需學習任何新組件的API

這與 Material-UI 或者 Ant Design 等傳統組件庫還略有不同,傳統意義的“統一 API”通常意味着庫的作者為所有組件定義了一套共享式統一的 Prop 接口,例如 variantcolorsize 等

而 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.RootDialog.TriggerDialog.ContentDialog.Close,無論開發什麼具體組件,這些結構都是可複用的,開發者通過組合這些部分來構建完整的組件。這種構建模式是固定的、可預測的。

而狀態上,Radix 為狀態管理提供了統一模式,開發者可以讓組件自我管理狀態(非可控),也可以通過 openonOpenChange 等 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 (variantsize) 返回對應的 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> 元素
  • 使用 asChildasChild 屬性告訴 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'value400 }, { name'B'value300 }];
<ChartContainer
  title="月度收入"
  description="最近六個月的收入趨勢"
  chartData={myData}
/>

這段樣例展示了設計模式中的 封裝與適配器模式,將一個第三方庫(recharts)的 API 和樣式,封裝成了符合我們自己項目設計規範的組件。

<BarChart data={chartData} ...> 體現了適配器模式的核心——轉換接口

最終,我們達成了以下優勢:

  • 易使用: 如果不封裝,開發者需要每次都寫一遍 CardCardHeaderBarChart... 的所有模板代碼。而現在只需要調用 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> 一樣給它傳遞 placeholderidnamevalueonChangeonFocusdisabled 等等,無需在 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: 0y: -50scale: 0.9 }}
      animate={{ opacity: 1y: 0scale: 1 }}
      transition={{ type: "spring", stiffness: 260damping: 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 installnpm 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]