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。
先假設沒有 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 System 裡會重複使用的設計決策取名字。
不要讓每個 Component 自己決定圓角,而是定義一套 Radius:
--radius-sm: ...;
--radius-md: ...;
--radius-lg: ...;
於是 Component 不需要知道真正的數值,只需要知道自己應該使用哪一個 Token。
例如:
Button
↓
primary
↓
某個實際顏色
未來如果品牌色改變,Component 本身不一定需要跟著修改,改的是 Token。
初始化 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 真正使用顏色的方式。
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。
講到 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 一起逐步長出來。
除了 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
這時有一個誘惑:
「好,既然今天叫 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 也用一下。
最後又回到沒有規則的狀態。
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 就越多。
做到這裡,這套 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 的原因之一。
今天沒有新增第二顆 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,可以一起用嗎?
iThome鐵人賽