iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
自我挑戰組

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

Day 5: 在做元件之前,先建立自己的 Design Tokens

  • 分享至 

  • xImage
  •  

Day 4 拆開第一顆 shadcn/ui Button 時,可以看到不少這樣的 Class:

bg-primary
text-primary-foreground
bg-secondary
border-border
focus-visible:ring-ring

Button 並沒有直接寫:

bg-blue-600
text-white
border-gray-300

它只知道:

我是 Primary Button,所以使用 primary。
我的文字放在 Primary Background 上,所以使用 primary-foreground。

至於 primary 到底是藍色、紫色還是其他顏色,Button 本身不需要知道。

這就是今天要處理的 Design Tokens。


如果每顆 Component 都自己決定 Style

先假設沒有 Design Tokens。

今天做 Button:

background: #2563eb;
border-radius: 8px;

明天做 Input:

border: 1px solid #d1d5db;
border-radius: 6px;

後天做到 Select:

border: 1px solid #cccccc;
border-radius: 0.5rem;

它們可能看起來都差不多。

但慢慢就會出現:

Button  → 8px
Input   → 6px
Select  → 0.5rem

Border A → #d1d5db
Border B → #cccccc
Border C → #ddd

這個畫面似曾相識。

因為這正是 Day 1 想解決的問題之一:

每個系統、甚至每個 Component,都有自己的一套規則。

所以在繼續做第二顆、第三顆 Component 之前,先停下來處理這些共用的設計決策。


Design Token 是什麼?

Design Token 可以先很簡單地理解成:

替 Design System 裡會重複使用的設計決策取名字。

不要讓每個 Component 自己決定圓角,而是定義一套 Radius:

--radius-sm: ...;
--radius-md: ...;
--radius-lg: ...;

於是 Component 不需要知道真正的數值,只需要知道自己應該使用哪一個 Token。

例如:

Button
  ↓
primary
  ↓
某個實際顏色

未來如果品牌色改變,Component 本身不一定需要跟著修改,改的是 Token。


shadcn/ui 其實已經替我們準備了一套 Tokens

初始化 shadcn/ui 之後,打開目前 Project 的 src/index.css,可以看到不少 CSS Variables。

例如:

:root {
  --background: ...;
  --foreground: ...;

  --primary: ...;
  --primary-foreground: ...;

  --secondary: ...;
  --secondary-foreground: ...;

  --muted: ...;
  --muted-foreground: ...;

  --destructive: ...;

  --border: ...;
  --input: ...;
  --ring: ...;

  --radius: ...;
}

實際內容會依初始化時選擇的 Preset、Base Color 等設定有所不同。

這些名稱跟昨天 Button 裡看到的:

bg-primary
text-primary-foreground
border-border
focus-visible:ring-ring

已經開始對上了。


描述的是:

這裡使用系統的主要操作色。

兩者看起來可能暫時是同一個顏色,但意思不一樣。

可以把 Token 粗略想成兩層:

Primitive Token
       │
       ▼
   blue-600
       │
       ▼
Semantic Token
       │
       ▼
    primary
       │
       ▼
   Component

Primitive Token 比較接近「這個值是什麼」:

blue-600
gray-100
radius-8

Semantic Token 則比較接近「這個值拿來做什麼」:

primary
background
foreground
border
destructive
ring

Component 最好盡可能依賴後者。

因為 Design System 真正想統一的,不只是「大家都用同一個藍色」,而是:

什麼情境應該使用什麼角色的顏色。


primary 和 primary-foreground 為什麼是一組?

shadcn/ui 的 Theme Token 有一個很容易理解的 Convention:

primary
primary-foreground

primary 是 Surface / Background Color。

primary-foreground 則是放在這個 Surface 上的文字或 Icon Color。

所以:

<div className="bg-primary text-primary-foreground">
  Hello
</div>

可以理解成:

┌─────────────────────────┐
│       Hello             │
│                         │
│ Background:primary     │
│ Text:primary-foreground│
└─────────────────────────┘

同樣的概念也可以看到:

background / foreground

card / card-foreground

popover / popover-foreground

secondary / secondary-foreground

accent / accent-foreground

這比單純定義「Blue、Gray、White」更接近 Component 真正使用顏色的方式。


那 Tailwind 的 bg-primary 是哪裡來的?

這裡一開始有點容易混亂。

CSS 裡除了:

:root {
  --primary: ...;
}

還可以看到類似:

@theme inline {
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);

  --color-background: var(--background);
  --color-foreground: var(--foreground);

  --color-border: var(--border);
  --color-ring: var(--ring);
}

這一層是在把 Theme Variables 暴露給 Tailwind。

所以大致可以理解成:

:root

--primary
    │
    ▼
@theme inline

--color-primary
    │
    ▼
Tailwind Utility

bg-primary
text-primary
border-primary

因此昨天 Button 裡的:

bg-primary

最後才能找到 Theme 裡真正的顏色。

Tailwind CSS v4 把 Theme Configuration 很大一部分搬進 CSS,Theme Variables 會直接影響可以使用的 Utility Classes。


第一版 Token 不需要把全世界都定義完

講到 Design System,很容易開始列:

Color
Typography
Spacing
Radius
Shadow
Breakpoint
Motion
Z-index
Opacity
...

然後 Day 5 就寫不完了。

目前這套 UI Kit 才剛有第一顆 Button,所以第一版先從 Component 已經真的需要的東西開始。

例如:

Color
├─ background
├─ foreground
├─ primary
├─ primary-foreground
├─ secondary
├─ secondary-foreground
├─ muted
├─ muted-foreground
├─ destructive
├─ border
├─ input
└─ ring

Radius
├─ sm
├─ md
├─ lg
└─ ...

Typography、Spacing、Shadow 等等當然也屬於 Design Tokens,但不需要今天一次決定完。

Design System 不是第一天就寫好一本完整規格書。

它應該跟 Component 一起逐步長出來。


Radius 也可以是一套規則

除了 Color,shadcn/ui 目前也已經提供一個很好的例子:Radius。

Theme 裡可以有一個 Base Radius:

--radius: ...;

再由它衍生不同尺寸:

--radius-sm
--radius-md
--radius-lg
--radius-xl

概念大概是:

             --radius
                │
       ┌────────┼────────┐
       ▼        ▼        ▼
 radius-sm  radius-md  radius-lg

這樣未來如果整套 UI 想從比較圓潤
不需要一顆一顆 Component 找 rounded-* 修改。

應該先回來看 Token


那自己的 UI Kit 到底要改什麼?

這時有一個誘惑:

「好,既然今天叫 Design Tokens,那來選品牌色!」

然後開始在 Color Picker 裡玩半天。

但目前還不急著決定「最好看的藍色」。

第一版比較重要的是先確定 Token 的角色。

例如:

background
頁面的主要背景

foreground
主要文字

primary
主要 Action / Brand Color

primary-foreground
Primary Surface 上的內容

secondary
次要 Action / Surface

muted
較低強調度的 Surface

muted-foreground
次要文字

destructive
危險或破壞性操作(警告性)

border
一般 Border

input
Form Control Border / Surface

ring
Focus Ring

先知道每個 Token「負責什麼」,之後才有辦法討論它「應該長什麼顏色」。

否則很容易變成:

這個藍很好看,所以這裡也用。
那個灰好像不錯,Input 也用一下。

最後又回到沒有規則的狀態。


Accessibility Check:Token 也會影響 Accessibility

Design Tokens 看起來很像純視覺設計,但其實跟 Accessibility 有直接關係。

例如昨天 Button 使用:

bg-primary
text-primary-foreground

如果把 primary 換成比較淺的顏色,卻沒有一起確認 primary-foreground,就可能讓文字和背景的 對比度 不足。

Focus 也是:

focus-visible:ring-ring

如果 ring 和 Background 太接近,即使程式碼裡確實存在 Focus Style,使用者還是可能很難看見目前焦點在哪裡。

所以之後修改 Color Token 時,不能只問:

好不好看?

還需要問:

□ foreground / background 對比是否足夠?
□ primary / primary-foreground 是否容易閱讀?
□ destructive 狀態是否只靠顏色表達?
□ ring 在不同背景上是否清楚?
□ Light / Dark Theme 是否都成立?

這也是為什麼 Accessibility 不應該等到網站準備申請標章時才開始處理。

如果問題來自 Design Token,那麼越晚發現,受影響的 Component 就越多。


Token 會是 React 和 Legacy Web 的交會點

做到這裡,這套 UI Kit 原本規劃的另一個目標也開始比較清楚了。

未來並不是只有 React 會使用它。

                   Design Tokens
                         │
              ┌──────────┴──────────┐
              ▼                     ▼
         React Component        Legacy Web
              │                     │
          shadcn/ui              CDN / CSS

React Component 可以使用:

bg-primary

Legacy Web 未來不一定會使用 React,也不一定會使用 Tailwind。

但它仍然可以使用同一套底層 CSS Variables:

var(--primary)
var(--border)
var(--ring)

這代表真正把新舊系統連在一起的,不一定是 React Component 本身。

Design Tokens 反而可能是最底層、也最容易共用的一層。

這也是這套 UI Kit 不只想做成 React Component Library 的原因之一。


Day 5 Done

今天沒有新增第二顆 Component。

甚至連昨天的 Button 都還沒有正式開始改:}

但現在開始有了一層比 Component 更底層的東西:

                 Design System
                      │
               Design Tokens
                      │
          ┌───────────┴───────────┐
          ▼                       ▼
       Button                   Input
          │                       │
       primary                  border
       radius                   ring
       ring                     radius

今天再往下一層,開始理解那些 Style 不應該只是散落在 Component 裡的一堆數值,而應該來自共同的 Design Tokens。

目前 Project 裡已經同時出現 Tailwind CSS、CSS Variables,之後還預計使用 SCSS。

這三個東西到底要怎麼分工?

如果全部都能寫 Style,又要怎麼避免最後變成三套規則?

Day 6: Tailwind CSS + SCSS,可以一起用嗎?


上一篇
Day 4: 拆開 shadcn/ui Button:一顆元件裡到底有什麼?
下一篇
Day 6: Tailwind CSS + SCSS,可以一起用嗎?
系列文
30 天打造 Accessible UI Kit:從 shadcn/ui 到自己的 Design System 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言