承昨日把代幣同步進程式碼的那條管線,今天走到管線的終點站:元件。代幣是設計系統的「詞彙」,而元件的 API 才是這些詞彙被實際說出口的地方。一個 variant="primary" 的按鈕,背後接的正是語意代幣 color.bg.primary;元件 API 設計得好不好,決定了設計師與工程師是用同一套詞彙溝通,還是各說各話。今天談元件的公開介面——props 怎麼命名、variants 怎麼切、行為與樣式怎麼拆——這是設計系統從「有代幣」走到「好用」的關鍵一哩路。

📖 學

元件 API 的第一原則,是用「意圖」而非「外觀」命名 variant。variant 這個 prop 最常見的值是 primarysecondaryghostdanger,而不是 bluegreyred。這個選擇和昨天的語意代幣是同一件事的兩面:在使用端你不需要知道 secondary 對應哪個 hex,正如語意代幣讓元件不必碰原始色。語意化命名的好處在改版時才會兌現——當品牌把主色從藍換成綠,variant="primary" 一行不用動,因為它描述的是「這顆按鈕的角色」,不是「它現在是什麼顏色」。外觀名一旦寫進 API,就等於把今天的視覺決定焊死進了所有呼叫端。

props 命名的第二個慣例,是用連綴詞(linking verb)前綴表達布林狀態:isDisabledisLoadinghasErrorisSelected。這讓 prop 的型別與意圖在讀到名字的當下就清楚——isLoading 一看就知道是布林、是狀態;若寫成 loadingerror 則語意含糊。更重要的是跨元件一致性:如果 Button 用 variantsize,那 Badge、Card、Input 也都該用同一組名字;若 Card 接受 className,每個元件就都該接受。一致性的價值在於,開發者只要學會一套模式,就能套用到整個系統,不必為每個元件重讀文件。這是設計系統作為「介面」該有的可預測性。

第三個要內化的判準,是「用 props 處理樣式變化,用組合(composition)處理結構彈性」。經驗法則是:如果一個 prop 控制的是外觀(顏色、尺寸、圓角),用 prop 沒問題;但如果它想控制的是結構或內容(例如卡片裡要放圖、放標題還是放操作列),那就該用組合——開放 children 或具名的 slot 子元件,而不是不斷長出 hasImagehasFootershowAvatar 這類布林。布林 prop 的數量是一個健康警訊:當一個元件的布林 props 開始爆炸、彼此還會互斥,通常代表你該把它拆成幾個可組合的子元件。Material UI 的 API 指南把這條講得很直白——優先組合、審慎新增 prop。

variants 的實作在 2026 年已經有成熟的工具層。以 Tailwind 為核心的專案,主流是 CVA(class-variance-authority):你用它宣告 base 類別、各個 variant 維度(variantsize)、以及 compound variants(某兩個維度同時成立時才套用的規則),它回傳一個型別安全的函式,吃 props 吐 className。CVA 通常搭 tailwind-merge 處理外部傳入 class 的衝突覆蓋,這組合正是 shadcn/ui 的底層。若需要更多——slots(一個元件多個可命名的樣式插槽)、內建 merge、design-system 導向的 API——則會往 Tailwind Variants 走,像 HeroUI 這類元件庫就建在其上。無論用哪個,核心心智模型一致:把「有哪些 variant」宣告成一份可列舉、可被 TypeScript 檢查的 recipe,而不是散在 JSX 裡的三元運算子。

最後一層,是把「行為/無障礙」和「樣式/variant」分開治理。2026 年的共識是用 headless(無頭)元件庫承接前者:Radix Primitives 與 React Aria 提供鍵盤互動、焦點管理、ARIA 語意與狀態機,但完全不帶樣式,樣式由你用代幣與 CVA 補上。兩者定位略有不同——Radix 以務實的開發體驗與良好無障礙見長,是多數產品開發的預設,也是 shadcn 的根基;React Aria(Adobe)則追求最嚴謹的無障礙與國際化,涵蓋 40 多種元件模式,適合有 WCAG AA 硬性要求或多語系使用者的場景,新的 react-aria-components 也提供了接近 Radix 的複合元件 API。這個分層的意義是:你的 variant API 只需操心視覺,無障礙這種「做錯了很貴、做對了看不見」的部分交給專門的行為層,兩者在同一個元件裡各司其職。

🧠 記

  • variant 用意圖命名不用外觀命名:primary/secondary/danger,不是 blue/red;改版時角色名不動、顏色靠語意代幣換。
  • 布林狀態用連綴詞前綴:isDisabledisLoadinghasError;跨元件共用同一組 prop 名(variantsizeclassName)換取可預測性。
  • 樣式變化用 props,結構彈性用組合;布林 props 爆炸且互斥,是該拆成可組合子元件的信號。
  • variants 實作用 recipe 工具:CVA(base + variants + compound variants)搭 tailwind-merge,是 shadcn/ui 底層;要 slots 就上 Tailwind Variants。
  • 行為與樣式分層:Radix / React Aria 負責鍵盤、焦點、ARIA 等無障礙,你的 variant API 只管視覺。
  • variant 值接語意代幣,元件 API 因此成為代幣詞彙的實際使用端,是設計系統從「有代幣」到「好用」的一哩路。

✍️ 實踐

挑一個你系統裡的 Button(或任何有多種樣式的元件),做以下三件事,約 15 分鐘。

  • 盤點它現在的 props:把每個 prop 標成「樣式」或「結構/內容」兩類。找出任何用外觀命名的 variant 值(如 bluegrey),把它改成意圖名(primarysecondary);找出散落的布林(borderedfilled),評估是否該收斂成單一 variant 列舉。
  • 檢查布林狀態命名是否加了連綴詞前綴:把 disabledloading 之類統一成 isDisabledisLoading,並確認同系統的其他元件也用同一套慣例。
  • 把這顆按鈕的樣式改寫成一份 CVA recipe:宣告 base、variantsize 兩個維度,並試著寫一條 compound variant(例如「variant=ghostsize=sm 時」的特例),讓 TypeScript 幫你把可用組合列舉出來。動手前先讀一次 cva.style 首頁範例。

🔗 延伸學習

💬 問 AI

我有一個 React 元件庫,底層代幣已分成原始/語意/元件三層。
現在要整理元件的公開 API。請幫我:
1. 檢視我這顆 Button 的 props,逐一標記為「樣式類」或「結構/內容類」,
   並指出哪些 variant 值用了外觀命名、該改成什麼意圖名;
2. 找出可以收斂成單一 variant 列舉的分散布林 props,
   以及哪些布林 props 其實該改用組合(children / slot 子元件);
3. 把樣式改寫成一份 class-variance-authority 的 recipe:
   base、variant、size 兩個維度,並示範一條合理的 compound variant;
4. 建議 variant 值如何對應到我的語意代幣,讓改版換色時 API 不用動;
5. 說明無障礙(鍵盤、焦點、ARIA)這層我該交給 Radix 還是 React Aria,為什麼。
請用繁體中文、台灣用語,每一步給可今天就動手的具體做法與程式碼片段。