iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0
自我挑戰組

30 天打造 Accessible UI Kit:從 shadcn/ui 到自己的 Design System系列 第 30 篇

Day 30: 從一顆 Button 到 Design System 的起點:30 天打造 Accessible UI Kit 完賽回顧

  • 分享至 

  • xImage
  •  

終於來到 Day 30 了!🎉

30 天前,我為自己設定了一個題目:

30 天打造 Accessible UI Kit。

一開始的想法其實很單純。

我希望建立一套自己能持續使用的 UI 元件,不必每次開發新專案時,都重新處理 Button、Input、Dialog 等常見介面。

但又不想只是把幾個 React Components 包裝起來。

因為在實際開發過程中,我發現 UI 元件真正麻煩的地方,往往不只是外觀。

還包含:

  • 不同元件之間的視覺一致性。
  • 表單欄位與錯誤訊息的關聯。
  • 鍵盤操作與 Focus Management。
  • 不同狀態下的 Accessibility。
  • 既有網站與新專案之間的技術差異。
  • 元件文件與使用規範的維護。

再加上現在 AI Coding Assistant 已經逐漸成為開發流程的一部分,我也開始思考:

如果 AI 能直接理解 UI Kit 的使用規範,是不是就能減少它每次重新產生一套元件的情況?

於是,這個專案慢慢從「做一套 React UI Kit」,延伸成了幾個不同的目標:

                        CUI
                         │
                  Design Tokens
                         │
                  Component Contract
                         │
           ┌─────────────┼─────────────┐
           ↓             ↓             ↓
         React          Legacy       AI Skill
           │             │             │
       Components      HTML/CSS      Guidelines
           │             │             │
           └─────────────┼─────────────┘
                         │
                    Storybook
                         │
                Accessibility Testing

這就是這 30 天一直希望逐步建立的 CUI 架構。

今天,我想從頭整理這段開發歷程,回顧目前完成的內容、遇到的問題,以及這個專案接下來還有哪些值得繼續努力的方向。


1. 為什麼要自己做一套 UI Kit?

現在其實已經有非常多成熟的 UI Libraries。

例如:

  • MUI
  • Ant Design
  • Chakra UI
  • shadcn/ui

它們都有各自的設計理念與適用情境。

如果只是想快速完成一個 React 專案,直接選擇成熟的方案,通常會比自己從零開發更有效率。

那為什麼還要做 CUI?

因為我希望透過這次挑戰,真正理解:

一套 UI Kit 除了元件本身,還需要哪些東西?

例如一顆 Button。

如果只是完成:

<button className="...">
  儲存
</button>

可能不需要花太多時間。

但如果希望它能長期維護,就必須進一步考慮:

Button
│
├── Design Tokens
├── Variants
├── Sizes
├── States
├── Keyboard Behavior
├── Accessible Name
├── Focus Indicator
├── Documentation
└── Testing

這些事情單獨看起來都不算特別困難。

但當它們需要彼此一致,而且未來修改其中一項時,不能影響其他部分,複雜度就會明顯提高。

所以這次挑戰的重點,並不是證明自己可以做出比成熟 UI Library 更好的元件。

而是學習如何建立一套有規範、可維護、能持續擴充的元件系統。


2. 技術選型:不是真的從零開始

CUI 使用的主要技術包含:

React
TypeScript
Vite
Tailwind CSS
shadcn/ui
Base UI
CVA
Storybook

其中,shadcn/ui 是這次非常重要的起點。

因為它不是傳統意義上,安裝後只能透過套件 API 使用的元件庫。

它讓開發者把元件原始碼加入自己的專案,並依照需求調整。

這很符合我這次的目標。

我希望能實際閱讀元件的實作,理解它的組成,而不是只會使用外部提供的 Props。

另外,CUI 採用的是以 Base UI 為基礎的元件實作。

這也讓我在處理 Dialog、Select 等互動元件時,能夠將底層互動行為交給較成熟的 Primitive,再專注於 CUI 自己的設計規範。

整體概念是:

             CUI Component
                   │
          ┌────────┴────────┐
          ↓                 ↓
      Appearance         Behavior
          │                 │
   Design Tokens         Base UI
   Tailwind CSS          Native HTML
   Component Styles      ARIA Patterns

這裡最重要的決定是:

不把所有底層互動行為都重新實作一次。

例如 Dialog 的 Focus Management、Select 的 Keyboard Interaction,本來就有許多細節需要處理。

如果為了「自己做 UI Kit」,連這些基礎行為都全部重寫,反而可能增加 Accessibility 風險。

所以 CUI 選擇站在既有工具的基礎上,建立自己的設計與使用規範。


3. 第一階段:先把 Design Tokens 建立起來

回頭看,我覺得最值得慶幸的決定之一,就是沒有一開始就急著大量製作元件。

而是先整理 Design Tokens。

因為如果每個元件都自己決定:

background: #2563eb;
color: #ffffff;
border-radius: 6px;

未來要統一修改,就會非常麻煩。

所以我們逐步建立了 Semantic Tokens。

例如:

:root {
  --cui-primary: ...;
  --cui-on-primary: ...;

  --cui-surface: ...;
  --cui-on-surface: ...;

  --cui-error: ...;
  --cui-on-error: ...;
}

這裡的重點不是變數名稱本身。

而是把顏色從「某個具體色碼」,轉換成「具有用途的設計語意」。

例如:

Primary
→ 主要操作

Error
→ 錯誤狀態

Surface
→ 介面表面

On Surface
→ 表面上的內容

這樣未來不論是 React Component,還是傳統 HTML 頁面,都可以使用同一套設計語言。

Design Tokens 不只是為了換主題

一開始很容易把 Design Tokens 理解成:

以後要改顏色比較方便。

但實際做下來,我覺得它還有另一個重要價值:

讓 Accessibility Requirements 有機會成為設計系統的一部分。

例如文字與背景的對比度,就不需要每次開發元件時才重新思考。

我們可以先針對常用的 Semantic Color Pair 進行檢查,再讓元件重複使用。

這也是後來建立 Contrast Check 的原因。


4. 第二階段:從 Button 開始建立 Component Pattern

完成 Tokens 後,我們開始處理 Button。

Button 看起來是最簡單的元件之一。

但實際拆解後,會發現它包含不少設計決策。

例如:

Variant
├── primary
├── secondary
├── outline
├── ghost
├── destructive
└── link

以及:

Size
├── sm
├── md
├── lg
├── icon-sm
├── icon-md
└── icon-lg

這些設定可以透過 CVA 管理。

例如:

<Button variant="primary" size="md">
  儲存
</Button>

但我在這個階段學到的一件事是:

Variant 並不是越多越好。

如果每次遇到新的畫面需求,就新增一個 Variant,最後可能出現:

primary
primary-light
primary-soft
primary-alt
primary-special

這樣不但難以維護,也會讓使用者不知道應該選哪一個。

所以 UI Kit 應該盡量讓每個 Variant 都有清楚的用途。

而不是單純提供大量不同的外觀選項。


5. 第三階段:Accessibility 不能等到最後才補

這次挑戰最核心的主題,就是 Accessibility。

以前在開發一般介面時,很容易先完成:

Appearance
↓
Interaction
↓
Accessibility Check

但這次我希望反過來思考。

當我們建立一個元件時,就同時確認:

Semantic HTML
Accessible Name
Keyboard
Focus
State
Error Handling

例如 Icon-only Button:

<Button
  size="icon-md"
  aria-label="搜尋"
>
  <SearchIcon aria-hidden="true" />
</Button>

如果只有 Icon,卻沒有 Accessible Name,視覺上可能完全看不出問題。

但對使用 Screen Reader 的使用者而言,這顆 Button 的用途可能不清楚。

再例如表單:

<label htmlFor="email">
  電子郵件
</label>

<input
  id="email"
  name="email"
  type="email"
/>

這些都是看似很小的細節。

但如果在 UI Kit 層級就建立正確範例,未來每次使用元件時,就比較不容易重複犯同樣的錯誤。

Accessibility 不等於加很多 ARIA

這也是我在這次挑戰裡不斷提醒自己的事情。

不是每個元素都需要:

role="..."
aria-label="..."
aria-describedby="..."

很多時候,正確使用原生 HTML 就已經能提供必要的語意。

例如:

<button type="button">
  儲存
</button>

通常就比:

<div role="button">
  儲存
</div>

更適合一般操作按鈕的情境。

因為原生 Button 已經具有對應的鍵盤行為與語意。

所以這次 CUI 很重要的一個原則就是:

Semantic HTML First, ARIA When Needed。


6. 第四階段:元件不只有 Default State

隨著元件越做越多,我也開始注意到另一個問題。

很多元件在展示時,通常只會看到最理想的狀態。

例如:

Input
→ 有值且沒有錯誤

Table
→ 有資料

Button
→ 可以點擊

但實際使用者不一定永遠遇到這些情境。

例如資料列表還可能有:

Loading
Empty
No Results
Error

表單也可能有:

Required
Invalid
Disabled
Read Only

所以 UI Kit 不能只定義元件的外觀。

還要考慮它在不同情境下的使用方式。

尤其是 Loading 與 Error State。

例如使用者送出表單後:

Submit
↓
Loading
↓
Success / Error

如果畫面只有一個轉圈動畫,卻沒有適當的狀態訊息,使用者可能不知道系統目前正在做什麼。

這讓我開始理解:

Accessibility 不只是單一元素的屬性,也包含整個操作流程是否能被理解。


7. 第五階段:Component Contract

到了後半段,我們開始整理 Component Contract。

這是整個專案裡,我覺得非常重要的一個轉折。

因為在這之前,我們主要是在思考:

這個元件要怎麼實作?

但開始整理 Contract 後,問題變成:

不同的使用者與環境,應該如何正確使用這個元件?

例如 Button 的 Contract 可以包含:

Button Contract
│
├── Semantic Element
│   └── button
│
├── Public API
│   ├── variant
│   └── size
│
├── State
│   ├── disabled
│   └── focus-visible
│
├── Styling
│   └── Design Tokens
│
└── Accessibility
    ├── Accessible Name
    └── Keyboard Behavior

另外,我們也開始整理:

data-cui-slot
data-cui-variant
data-cui-size

這些公開的 HTML Styling Contract。

例如:

<button
  type="button"
  data-cui-slot="button"
  data-cui-variant="primary"
  data-cui-size="md"
>
  儲存
</button>

這讓 CUI 不再只能透過 React Props 表達元件設定。

也開始有機會讓一般 HTML 使用同樣的設計規範。


8. 第六階段:Storybook 與 Accessibility Testing

接著,我們加入了 Storybook。

在這之前,元件展示主要集中在 App.tsx。

隨著元件數量增加,這種方式會越來越難維護。

例如要測試 Button:

Primary
Secondary
Disabled
Icon Only

可能需要一直修改 JSX,或在同一個頁面放上大量測試元件。

Storybook 則讓我們可以把每個使用情境整理成獨立的 Story。

Storybook
│
├── Components
│   ├── Button
│   ├── Input
│   ├── Alert
│   └── Dialog
│
└── Patterns
    ├── Form
    └── Data List

這樣不只方便展示。

也讓不同狀態更容易被重複檢查。

自動化測試不是 Accessibility 的終點

Day 26,我們開始整理 Accessibility Testing。

包含:

Automated Testing
Interaction Testing
Manual Testing

例如 Axe 可以協助找出常見的 ARIA、Label 與部分 Color Contrast 問題。

但它無法完整判斷:

  • Accessible Name 是否符合實際用途。
  • Dialog 關閉後 Focus 是否回到合理位置。
  • 鍵盤使用者能否順利完成整個流程。
  • Screen Reader 使用者是否能理解狀態變化。

所以我們不能把:

No Violations

直接理解成:

Fully Accessible

這次挑戰讓我更加確定:

Accessibility Testing 應該是一個持續進行的流程,而不是完成元件後的一次性檢查。


9. 第七階段:讓 CUI 不只存在於 React

這是我從一開始就很想嘗試的方向。

因為不是所有網站都使用 React。

有些既有網站已經有自己的 HTML、CSS 與 JavaScript。

如果為了使用一套 UI Kit,就必須先更換整個前端架構,那導入成本可能太高。

所以 Day 27、Day 28,我們開始建立 CDN CSS 的輸出流程,以及傳統 HTML 的整合範例。

例如:

<link
  rel="stylesheet"
  href="/assets/cui.min.css"
>

接著:

<button
  type="button"
  data-cui-slot="button"
  data-cui-variant="primary"
  data-cui-size="md"
>
  儲存
</button>

不需要 React Runtime,也不需要 JSX。

但這裡也讓我重新理解了:

Single Source, Multiple Outputs,不代表所有環境都要使用完全相同的程式碼。

React 可以使用 React Component。

傳統 HTML 可以使用公開的 Styling Contract。

兩邊真正需要共享的是:

Design Tokens
Component Styles
Semantic Rules
Accessibility Requirements

而不是強迫所有環境使用相同的 Framework。

CSS 共用比想像中更需要規劃

例如 React Button 原本透過:

CVA
+
Tailwind Utilities

管理樣式。

如果又為 Legacy 另外寫一套 Button CSS,很容易產生兩份需要同步維護的樣式。

因此我們開始思考如何把跨環境共用的樣式整理出來。

這不是單純把 CSS 複製一份。

而是需要重新決定:

哪些屬於 Shared Styles?

哪些屬於 React-specific Styles?

哪些屬於 Component Behavior?

這也是目前 CUI 仍然需要持續完善的地方。


10. 第八階段:讓 AI 也能理解 CUI

Day 29,我們開始建立 AI Skill。

這個方向其實來自一個很實際的問題。

現在使用 AI Coding Assistant 時,我們可以很快請 AI:

幫我建立一個表單。

但如果 AI 不知道專案已經有 CUI,它可能會重新寫出一套 Button、Input,甚至自己實作 Dialog。

這樣反而破壞了原本希望維持的 Design System 一致性。

所以我們開始整理:

CUI Skill
│
├── SKILL.md
├── Component Reference
├── Accessibility Rules
├── Legacy HTML Reference
└── Examples

讓 AI 在產生程式碼時,有機會先確認:

有哪些現成元件?

支援哪些 Props?

哪些 Accessibility Rules 必須遵守?

React 和傳統 HTML 應該如何使用?

不過 AI Skill 也不是萬能的。

它不能保證 AI 永遠不會產生錯誤程式碼。

也不能取代:

TypeScript
Build
Lint
Accessibility Testing
Code Review

它更像是一份提供給 AI 的開發規範。

讓 AI 不必完全依賴推測,而是能依照專案既有的設計與元件 Contract 工作。


11. 回頭整理:CUI 現在的整體架構

經過 30 天的開發與整理,目前 CUI 的目標架構可以表示成:

                         CUI
                          │
                   Design Tokens
                          │
                   Shared Contract
                          │
          ┌───────────────┼───────────────┐
          │               │               │
          ↓               ↓               ↓
        React           CDN CSS         AI Skill
          │               │               │
     Components       HTML Styling     Guidelines
          │               │               │
          ↓               ↓               ↓
      New Projects     Existing Web    AI Assistants
          │               │               │
          └───────────────┼───────────────┘
                          │
                  Documentation
                          │
                      Storybook
                          │
                 Accessibility Tests

如果再拆成實際的專案結構,大致會是:

cui/
│
├── src/
│   ├── components/
│   │   └── ui/
│   │       ├── button.tsx
│   │       ├── input.tsx
│   │       ├── dialog.tsx
│   │       └── ...
│   │
│   └── styles/
│       ├── tokens.css
│       ├── theme.css
│       ├── cdn.css
│       └── components/
│           └── ...
│
├── .storybook/
│
├── examples/
│   └── legacy/
│
├── skills/
│   └── cui/
│       ├── SKILL.md
│       ├── references/
│       └── examples/
│
├── scripts/
│   └── build-cdn.mjs
│
└── package.json

這裡呈現的是這次挑戰逐步建立的架構方向。

實際專案仍然可以依照元件數量、文件維護方式與建置需求調整。


12. 那麼,CUI 已經是 Design System 了嗎?

這是我想在最後一天回答的問題。

如果從最簡單的角度來看,CUI 已經不只是幾個 React Components。

因為我們開始建立:

Design Tokens
Component Patterns
Accessibility Rules
Documentation
Testing
Usage Contract

這些確實都是 Design System 的重要組成部分。

但如果問:

CUI 現在是不是一套成熟的 Design System?

我覺得還不是。

因為真正成熟的 Design System,不只是技術上有元件與文件。

還需要考慮:

  • 元件的版本管理與相容性。
  • 公開 API 的穩定程度。
  • 元件的長期維護與更新策略。
  • 文件是否能持續與原始碼同步。
  • 不同產品與使用情境的驗證。
  • 完整的 Accessibility Testing。
  • 使用者回饋與元件改善流程。

尤其是 CUI 目前仍然以學習與實作探索為主要目的。

很多規範已經開始建立,但還需要透過實際專案驗證。

所以我會把目前的 CUI 定位成:

一套以 Accessibility 為核心、逐步朝 Design System 發展的 UI Kit。


13. 這 30 天最大的收穫

如果要從這 30 天挑出幾個最重要的收穫,我會選以下幾件事。

第一,先建立規範,再大量建立元件

一開始很容易想要快速增加元件數量。

例如:

Button ✓
Input ✓
Select ✓
Dialog ✓
Table ✓

但元件越多,不代表 UI Kit 越成熟。

如果每個元件都使用不同的命名方式、顏色或互動規則,後續維護反而更困難。

所以比起快速增加元件數量,我覺得更重要的是先建立:

Tokens
Naming
Variants
States
Accessibility

再逐步擴充。

第二,Accessibility 必須從設計階段開始

如果等到所有元件完成後,才開始檢查 Accessibility,很多問題可能已經深入元件結構。

例如:

錯誤的 HTML Semantics
不完整的 Keyboard Behavior
缺少 Accessible Name
不合理的 Focus Management

這些問題不一定能靠補幾個 ARIA Attributes 解決。

因此 Accessibility 應該從元件設計時就開始考慮。

第三,文件也是產品的一部分

如果一套 UI Kit 只有程式碼,沒有使用規範,其他開發者可能不知道:

應該用哪個 Variant?

錯誤訊息要怎麼顯示?

Dialog 應該怎麼組合?

哪些 Props 可以使用?

所以 Storybook、Component Contract、Usage Examples 都是重要的成果。

第四,跨環境支援需要清楚的邊界

React 與傳統 HTML 的執行環境不同。

如果硬要讓兩者共用所有實作,可能會讓架構變得複雜。

但如果完全分開,又容易出現兩套不一致的 UI。

所以這次最值得繼續探索的方向之一,就是:

共享設計規範與樣式來源,但允許不同環境採用適合自己的實作方式。

第五,AI 需要規範,而不是只需要 Prompt

AI 可以加快開發速度。

但如果沒有專案脈絡,也可能產生與既有架構不一致的程式碼。

因此讓 AI 理解:

Existing Components
Public APIs
Accessibility Rules
Validation Process

可能比每次都重新要求它「請符合專案風格」更有效。


14. 哪些地方還沒有完成?

雖然今天是 Day 30,但 CUI 並不會因為鐵人賽結束,就突然變成一套完整的產品。

目前還有不少值得繼續改善的方向。

項目 後續方向
Component Coverage 補齊更多常用元件與狀態
Accessibility 擴充鍵盤、Screen Reader 與互動測試
Storybook 完善元件文件與測試案例
CDN CSS 持續整理共用樣式與版本管理
Legacy Integration 驗證更多實際頁面與 CSS 相容性
Interactive Adapter 評估 Tooltip、Dialog 等非 React 互動方案
AI Skill 測試不同任務並改善指引
Release 建立套件發布、版本管理與 CI 流程

這些項目不一定要一次完成。

我更希望 CUI 可以在未來實際使用時,逐步發現問題、補充規範,再持續改善。


15. 如果重新開始,我會怎麼安排?

如果重新進行這次挑戰,我可能會更早整理 Component Contract。

因為前期主要在處理:

Component Implementation

直到後面才開始系統性地思考:

Public API
Styling Contract
Accessibility Contract

尤其當 CUI 要同時支援 React 與 Legacy 時,這些規範就變得非常重要。

如果能在開發初期先確定:

Token Naming
Variant Naming
State Naming
Public Attributes

後面進行 CDN 整合時,應該可以減少不少重構工作。

另外,我也會更早導入 Storybook。

因為當元件開始增加後,集中在 App.tsx 展示與測試,真的會逐漸變得難以管理。

如果從第一個 Button 就建立 Story,後續每新增一個元件,都可以順便補上:

Default
Variants
States
Accessibility Notes

整體開發流程應該會更一致。


16. 接下來的 CUI

鐵人賽結束後,我希望 CUI 能從「30 天的開發挑戰」,逐步變成真正可以重複使用的專案。

短期內,我想先把這幾件事情做好:

第一,整理與穩定現有元件。

確認 Props、Variants、States 與 Accessibility Requirements 是否一致。

第二,完善 Storybook。

讓它不只是展示頁面,而是能真正作為元件文件與測試環境。

第三,持續驗證 CDN 整合。

讓一般 HTML 頁面可以使用 CUI 的共用樣式,而不必依賴 React。

第四,改善 AI Skill。

透過實際的 AI Coding 任務,觀察規範是否足夠清楚,以及哪些地方容易讓 AI 誤解。

最後,再逐步建立:

Versioning
Release Process
CI
Documentation Maintenance

讓 CUI 不只是能夠運作,也能持續維護。


17. 30 天前與 30 天後

30 天前,我對 UI Kit 的想像,大概是:

UI Kit
  │
  ├── Button
  ├── Input
  ├── Select
  └── Dialog

但經過這次挑戰,我現在更傾向把它理解成:

                     UI Kit
                       │
                Design Language
                       │
                 Design Tokens
                       │
                Component Rules
                       │
          ┌────────────┼────────────┐
          ↓            ↓            ↓
      Components   Documentation   Testing
          │            │            │
          └────────────┼────────────┘
                       │
                 Consistency
                       │
                 Accessibility

元件只是其中一部分。

真正讓 UI Kit 具有長期價值的,是元件背後的規範、文件與測試。

而這些事情,也正是讓它逐步朝 Design System 發展的基礎。


18. 最後,給這 30 天一個總結

這次挑戰並沒有讓我在 30 天內完成一套成熟的 Design System。

但它讓我從一顆 Button 開始,逐步理解一套 UI Kit 需要考慮的事情。

從 Design Tokens 到 Component Contract。

從 Semantic HTML 到 Keyboard Interaction。

從 Storybook 到 Accessibility Testing。

從 React Components 到 CDN CSS。

最後再延伸到 AI Skill。

這些看似不同的主題,其實都圍繞著同一個目標:

讓介面更一致、更容易維護,也讓更多使用者能夠順利操作。

我想,這也是這次挑戰最有價值的地方。

Accessibility 不是額外加上的功能。

文件不是開發完成後才補的附屬品。

測試也不是只為了確認畫面沒有壞掉。

它們都應該是 UI Kit 設計與開發過程中的一部分。

而 Design System,也不是在完成一定數量的元件後,就會自動誕生。

它需要規範、驗證、維護,以及持續改善。

所以對現在的 CUI 來說,Day 30 不是終點。

而是一個起點。

30 天,從一顆 Button 開始。

接下來,讓 CUI 慢慢成為一套真正值得重複使用的 UI Kit。


完賽!

感謝這 30 天一路閱讀文章的大家。

如果你也正在建立自己的 UI Kit,或正在思考如何將 Accessibility 融入元件開發,希望這系列文章能提供一些不同的思考方向。

不一定需要從零建立所有元件,也不一定要一次完成整套 Design System。

從一個常用元件開始,建立清楚的規範、補上 Accessibility、整理文件,再逐步擴充,也是一種很好的方式。

最後,也替這次的 30 天挑戰畫下一個句點。

Day 30 Completed. 🎉

CUI — Accessible UI Kit.


上一篇
Day 29: AI Skill:讓 AI 也能正確使用 CUI
系列文
30 天打造 Accessible UI Kit:從 shadcn/ui 到自己的 Design System 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言