色彩語意
這一章講的不是色票,是意思。
Token 的名字說的是「這個顏色代表什麼」,不是「這是什麼顏色」。
所以沒有 --blue-500,只有 --primary、--danger、--field-editable。
換品牌色時改的是值,不是任何一行使用它的程式碼。理由見 ADR-0001。
四組語意,職責不重疊
1. 表面與文字(結構)
| Token | 意思 |
|---|---|
--background / --foreground | 頁面底色與主要文字 |
--card / --card-foreground | 卡片表面 |
--popover / --popover-foreground | 浮層表面(下拉、泡泡、篩選面板) |
--muted / --muted-foreground | 弱化表面與次要文字 |
--accent / --accent-foreground | hover/被指向的表面 |
--border / --input / --ring | 分隔線、控制項邊框、鍵盤聚焦環 |
深色模式下這五種表面構成抬升層次:background < card < popover < muted < border。
深色不是把淺色反轉——反轉出來的介面會扁平到分不出層。
2. 動作(控制項)
| Token | 用在哪 |
|---|---|
--primary | 主要動作(中性近黑,與色相主題無關) |
--secondary | 次要動作 |
--brand | 色相主題色:一頁一顆的關鍵動作、品牌強調 |
--brand-subtle | 主題色淡底:選中的導覽項、分頁底線區 |
--destructive | 破壞性動作的控制項(刪除鈕) |
primary 與 brand 為什麼分開品牌色為了視覺張力通常偏亮偏飽和,而後台畫面上按鈕很多。 兩者共用一個 token,等於讓高飽和色的出現面積失控—— 色彩疲勞管的是面積與頻率,不是色相的種類數。
所以 --primary 維持中性近黑,承擔絕大多數控制項;--brand 吃色相主題,
只給一頁一顆的關鍵動作。換主題時介面的重心不會整個變吵。
見 深淺主題 → 多色相主題。
3. 狀態(訊息與資料)
| Token | 意思 |
|---|---|
--success | 良好/已完成/通過 |
--warning | 需要注意,但不阻擋 |
--info | 中性提示/補充說明 |
--danger | 異常/錯誤/不合格 |
每個提醒色的完整定義、使用場景、反例與擴充程序,見提醒色辭典。
destructive 與 danger 為什麼分開一個是「按下去會刪東西」,一個是「這筆資料有問題」。 同一個畫面上兩者常常同時出現(一列紅色的異常資料,右邊一顆紅色的刪除鈕)。 混用會讓紅色失去意義——使用者不再知道紅色到底在警告什麼。
4. 欄位(可編輯 vs 唯讀)
| Token | 意思 |
|---|---|
--field-editable + --field-border | 這一格你可以改 |
--field-readonly | 這一格是算出來的/唯讀 |
--edit / --edit-bg / --edit-foreground | 已改動未送出(保留色) |
欄位色與 edit 的完整定義同樣收在提醒色辭典。
可編輯
唯讀/計算值
已改動未送出
欄位只分兩種:可編輯、唯讀。加上「已改動未送出」總共三種狀態, 使用者掃一眼就懂。
不要做「輸入/假設/公式」三色。那是試算表的儲存格慣例, 對網頁表單的使用者不成立——他不在乎這格背後是不是公式,他只在乎能不能改。 (這是實際踩過的坑,見 ADR-0002)
brand 的邊界:不要放在確認按鈕上
--brand 與 --primary 分開之後還有一個問題沒解決:brand 該出現在哪裡。
答案是不要出現在確認/送出/儲存這類提交性動作上。理由與 destructive/danger
分家完全相同——兩者職責不同:
| 職責 | |
|---|---|
--brand | 識別:這是誰的產品 |
| 確認按鈕 | 指示可供性:按下去會提交 |
讓一個 token 同時做兩件事就會產生違和。而且色相帶著既成慣例:
| 色相 | 慣例 | 放在確認按鈕上 |
|---|---|---|
| 綠 | 通行、完成 | 對齊 |
| 藍 | 系統預設動作(macOS/Windows 的預設鈕都是藍) | 對齊 |
| 紫/洋紅 | 沒有動作慣例 | 讀成裝飾,不像功能控制項 |
| 紅 | 停止 | 直接衝突(已被警報色域擋掉) |
| 灰 | 停用 | 反慣例 |
用 brand | 用 primary |
|---|---|
| logo 區、品牌強調 | 確認、送出、儲存、套用 |
| 「開始新流程」這類非提交型入口 | 一般操作 |
選中的導覽項(走 --brand-subtle) | 取消、返回 |
提交性動作在任何主題下都用 --primary,色相就永遠不會跟動作語意打架。
這也是六個色相全部保留的理由——違和來自把 brand 放錯位置,不是色相不能用。
Button 的停用態是 disabled:opacity-50,所以「停用」的實際外觀就是
--primary 以 50% 疊在背景上。一個接近中性的主題色做成填色按鈕,會正好撞進那個位置。
本書的預設主題「石墨」先前就是這樣:照公式生成一個 chroma 0.030 的灰藍, 量出來距停用外觀只有 ΔE00 8.5(低於 10 就是實務上分不開)——按鈕看起來像停用的。
修法不是把它加彩,而是承認它沒有品牌色:石墨的 --brand 直接鏡射 --primary。
npm run verify:color 有一條守衛盯著——ΔE00(brand, 停用外觀) ≥ 12。
紅黃要能讓人瞬間反應,主題要有識別度——兩者怎麼共存
這是色彩系統最常見的衝突:狀態色靠「一眼認出」工作,主題色靠「到處都是」工作。
衝突其實不存在,只要把三件事分開處理。 紅色能讓人瞬間停手,靠的不只是色相, 還有「它在畫面上很稀有、而且是最搶眼的東西」。所以主題不需要閃避紅色的長相, 需要閃避的是它的位置與份量。
一、色相層:警報色域是保留區
四個狀態色把暖色域整段吃滿——danger 18°、destructive 25°、warning 71°、edit 83°。
把 brand 的標準亮度掃過整個色相環,距所有狀態色 ≥25° 的區間只有三段:
| 可用區間 | 色系 | 寬度 |
|---|---|---|
| 109°–137° | 綠 | 28° |
| 188°–213° | 青 | 25° |
| 264°–352° | 藍紫 → 洋紅 | 88° |
0°–95° 一格都不剩。 這就是為什麼企業系統的主色壓倒性地是藍、靛、青—— 那不是流行,是剩下的地方只有那裡。
一個橙色或金色的主題,就算它的個別色票與 warning 分得開(明度差會把 ΔE00 拉大),
也等於把「瞬間反應」這個通道花在品牌上:畫面到處是暖色,紅色就不再突出。
二、飽和度層:警報必須是畫面上最飽和的東西
| OKLCH chroma | |
|---|---|
--danger | 0.223 |
--destructive | 0.208 |
--warning / --edit | 0.165 / 0.164 |
--brand(最飽和的主題) | 0.151 |
--primary(承擔多數控制項) | 0.040 |
danger 的飽和度是 brand 的 1.5 倍、primary 的 5.6 倍。
這個階序一旦反過來,紅色就從「最搶眼」降級成「其中一個彩色」。
三、面積層:稀有才有力量
60% 中性 / 30% 結構 / 10% 高飽和強調——提醒視窗的關鍵色與圖表的重點色共用那 10%。
這就是 --primary 維持中性近黑的理由:畫面上絕大多數按鈕不吃主題色,
於是一則紅色錯誤出現時,它是整個畫面唯一飽和的東西。若按鈕全部吃主題色,
紅色就得跟一整排彩色按鈕搶注意力。
這三條都由 npm run verify:color 擋
新增主題時會逐一檢查色相距離、飽和度階序、與各狀態色的感知距離。
實測擋掉過兩組候選:赤陶 45°(距 destructive 只有 20°,落在警報色域裡)、
松綠 178°(距 success 只有 16°)。
那就必須二選一:紅色當品牌,或紅色當警報,不能兩者都是。
多數後台系統的選擇是——品牌紅只出現在 logo 與登入頁,
進到系統內部之後 --brand 改用一個安全區間的色相。這不是妥協,
是因為在一個每天要處理異常的系統裡,「紅色=要處理」比「紅色=我們公司」值錢得多。
提醒視窗:用強度分級,不用更多顏色
四種語意封頂(success / warning / info / danger),不再擴充色相。
豐富度的來源是「同一個語意色的兩種強度」:
| 強度 | 樣式 | 用在哪 |
|---|---|---|
low(預設) | --{狀態}-subtle 淡底+與文字同色的左粗邊+圖示+--{狀態}-subtle-foreground | 日常與次要提示,大量出現也不刺眼 |
high | --{狀態} 實色滿版+--{狀態}-foreground 反白字 | 阻斷式訊息,必須停下來決定的操作 |
有疑慮就用 low。 高強度出現頻率一高,色彩疲勞的預算會瞬間爆掉——
疲勞管的是高飽和色的面積 × 頻率,不是色相的種類數。提醒視窗的關鍵色與圖表的重點色
共用同一份預算。
bg-danger/10 這種寫法的對比完全不可控,因為結果取決於底下是什麼表面。
本書上一版就是這樣做的,實測四種變體在淺色模式的文字對比只有 1.97–3.98:1,
全部低於 4.5——而且不會有任何東西報錯。
現在改成生成的實色 token,文字對底色反解到 4.5:1,npm run verify:color 逐一驗。
四種淡底必須彼此分得開
--{狀態}-subtle 刻意不做 harmonization(不往主題色相偏)。實測往主題拉 12°,
藍紫系主題的 warning-subtle 與 danger-subtle 會收斂到 ΔE00 8.8——
琥珀和紅都被拉成粉橘,「注意」和「錯誤」看起來變成同一種。
提醒視窗的整體感靠兩件事,都不是彎色相:四種共用同一條構成規則 (下一節),以及它們坐在帶主題色相的中性表面上。
「同一層」要等量,不是等參數
淡底的生成參數是染色量(ΔE00(淡底, 卡片表面)),不是彩度。
上一版是「固定明度 + 固定彩度」——數字很整齊,感知不整齊。原因是
sRGB 色域不是色相對稱的:在很亮的地方,綠與黃能撐住的彩度遠高於藍與紅,
於是 info 與 danger 被色域裁掉、success 沒有。實測結果:
| info | danger | warning | success | 最強/最弱 | |
|---|---|---|---|---|---|
| 舊(固定彩度) | 11.0 | 12.6 | 14.1 | 18.5 | 1.69× |
| 現在(固定染色量) | 14.5 | 15.1 | 15.0 | 14.8 | 1.04× |
舊的那一列順序是反的:綠色的「已全部完成」比紅色的「無法確認」還搶眼。 四種提示疊成一欄時這件事直接看得到,而且叫不出名字——這正是「突兀」的來源之一。
規則是兩段的:四種共用一個明度(所以看起來是同一家人),
各色相的彩度則反解到同一個染色量。彩度不能也固定,因為藍色在深色中性底上
本質上比較不顯眼,深色下 info 需要約 3.3 倍的彩度才追得上——
那個差距是對感知不對稱的補償,不是不一致。
npm run verify:color 有一條門檻:四種染色量的最大/最小比值不得超過 1.3。
左粗邊用文字色,不用實色
低強度的左粗邊是 border-l-current——與內文同色(--{狀態}-subtle-foreground),
不是全飽和的狀態色。兩個理由都是量出來的:
- 全飽和邊對自己的淡底只有 1.60–5.43:1,八組(四變體 × 兩模式)裡有四組低於 3:1 的非文字門檻(1.4.11)。改用文字色之後全部是 4.50–4.54:1—— 因為那個色本來就是對淡底反解到 4.5:1 的,所以「相等」是構造上保證的。
- 淡底的彩度約 0.03–0.08,全飽和邊卻是 0.134–0.223(3–6 倍)。 一個元件內部跳這麼大,就是使用者說的「突兀」。文字色的彩度約 0.100,跳幅收到 2 倍內。
欄位狀態:三層各司其職
一格欄位可以同時是「被聚焦」「改過沒送」「不合格」。這三件事走三個不同的通道, 疊起來互不干涉:
| 層 | 表達什麼 | 用什麼 |
|---|---|---|
| 聚焦環 | 鍵盤焦點在這裡(控制項語意) | --ring,恆定不隨狀態變色 |
| 邊框+底色 | 這格的狀態(狀態語意) | 三選一,有優先序 |
| 圖示+文字 | 為什麼 | aria-invalid + aria-describedby |
| 狀態 | 邊框 | 底色 | 優先序 |
|---|---|---|---|
| 唯讀/計算值 | — | --field-readonly | — |
| 可編輯 | --field-border | --field-editable | 0 |
| 已改動未送出 | --edit | --edit-bg | 1 |
| 不合格 | --danger | 不變(不整格染紅) | 2(最高) |
改過又不合格時,邊框顯示 --danger(阻擋性的優先);但「改過」不會消失,
它還在變更摘要與復原鈕上。同一個通道不傳兩件事。
不合格只動邊框不動底色是量出來的修正:整格染 --danger-subtle 時,
深色模式下 danger 邊框對那個底只有 2.42:1(低於 1.4.11 的 3:1)——
而且十個錯誤欄就是十塊紅底,高飽和面積預算當場爆掉。錯誤的主訊號是
邊框+圖示+文字(FormField 把這套固定寫法接好),
verify:color 有一條門檻盯著邊框對兩種欄位底的對比。
聚焦環永遠是同一個顏色。使用者靠「找同一個東西」就知道自己在哪。
不要讓聚焦環跟著驗證狀態變色。三種狀態會同時發生,變色就需要一張 沒人記得住的優先序表;而且 Tab 過三個必填空欄時每一格都閃紅, 這會訓練使用者忽略紅色。
ring-offset沒有 offset 時,環直接畫在元件邊框的外緣,於是它的對比要對邊框算。
實測環對 --danger 邊框在深色模式下只有 1.04:1——欄位一標成不合格,聚焦環當場隱形。
有 offset 時中間隔一圈背景色,環是對背景算,3:1 才成立。
npm test 有一條守衛:用了 ring-ring 卻沒有 ring-offset 的元件直接不過。
「必填未填」是不合格的一種,但它的問題其實是時機不是顏色—— 不該在使用者還沒碰過欄位時就標紅。慣例是 blur 或送出之後才標。
互動狀態的顏色邏輯
一條原則就夠:
每一個顏色變化都要對應一件使用者需要知道的事, 強度與那件事的「持續多久」成正比。
這一條同時擋掉兩種失敗。沒有事實要傳達就不變色,所以不會「為變而變」; 有事實就給到看得見的強度,所以不會「看不出目的」。
注意強度跟著持續時間走,不是跟著重要性走。瞬間的回饋要輕——它一秒後就消失, 用力沒有意義;持久的狀態才配得上明顯的顏色,因為使用者要一直看著它。
狀態階梯
| 狀態 | 持續多久 | 傳達什麼 | 走哪個通道 |
|---|---|---|---|
| hover | 指標在上面 | 這個可以互動 | 中性狀態層 6% |
| pressed | 按住的瞬間 | 系統收到了 | 中性狀態層 14% |
| focus | 直到移開焦點 | 鍵盤在這裡 | 環(完全不碰底色) |
| 已選 | 直到改選 | 你選了這個 | 中性狀態層 20% |
| 停用 | 直到條件改變 | 現在不能用 | 降低,不加色(opacity-50) |
| 已改動未送出 | 直到送出 | 還沒存 | 狀態色琥珀(見下一節) |
| 不合格 | 直到修正 | 這個值不能用 | 狀態色紅 |
四個判斷值得單獨講:
- 三個互動狀態全部走中性,不吃主題色。 高飽和色的出現面積是有預算的 (見上面的面積層)。hover 是滑過就沒的瞬間回饋,替它上主題色是最貴的用法。
- focus 走「環」這條獨立通道,不碰底色。 所以它能與任何狀態疊加而不打架—— 不合格+聚焦、已選+聚焦都成立。
- 停用是「移除可供性」,不是「增加訊息」,所以是降低而不是加色。 反過來說,任何常駐的半透明控制項都會被讀成不能按。
- 已選的列不得再用弱化文字。 20% 疊加之後
--muted-foreground掉到 2.77:1(淺)/2.96:1(深)。這不是調數值能解的——已選的列在語意上就是 被強調的,它的次要文字不該繼續弱化。規則:已選的列把--muted-foreground整個重新定義成--foreground。
狀態層是「疊加」,不是「換一組底色」
改版前 hover 用 --accent、已選用 --muted。問題是這三個 token
(--accent、--muted、--secondary)目前是同一個值——於是被指到的列、
斑馬列、唯讀區三者同色。實測:
| 淺色 ΔE00 | 深色 ΔE00 | |
|---|---|---|
| 一般 → hover | 1.6 | 2.5 |
| hover → 已選 | 1.6 | 2.3 |
ΔE00 低於 2 幾乎看不見。使用者分不出自己指在哪一列,也分不出 hover 與已選。
根因不是顏色選錯,是機制選錯:獨立的實色 token 是「取代」而不是「疊加」,
所以它無法與元件已有的底色組合。斑馬列本來就是 --muted,hover 換成同值的
--accent 等於什麼都沒做。
改成疊一層之後,「深了一階」這件事在每一種底色上都成立:
| 底色 | hover Δ | 已選 Δ |
|---|---|---|
| 頁面底 | 3.0 | 10.8 |
| 卡片 | 3.0 | 10.8 |
--muted(斑馬列) | 3.0 | 10.7 |
| 浮層 | 3.0 | 10.8 |
疊加的顏色取元件自己的 currentColor,不是統一的 --foreground。這一步是實測逼出來的:
深色模式下 --foreground 與 --primary 是同一個近白,拿它去疊實色按鈕會得到
ΔE00 0.0——按鈕完全沒有 hover 回饋,而四種表面的檢查照樣全綠。
取 currentColor 則自動是「深底配亮疊、亮底配深疊」,方向永遠對。
順帶把六種各行其是的透明度收成一組。改版前 hover 用了
/90、/80、/50、/40、/20、/15 六種值,可見度從 ΔE00 0.6
(次要按鈕,等於沒變)到 7.4(主按鈕,過於刻意)差了十倍;
改用同一組強度之後全部落在 2.6–4.5。
為什麼上限也要驗
四道門檻裡有一條是上限:已選對底色不得超過 ΔE00 16。 「太刻意」跟「看不出來」一樣是缺陷,而且只有上限擋得住它。
上限實際卡住的是深色模式的頁面底色——那是全系統最深的表面, 同一個 alpha 疊在它上面的感知落差最大(低亮度端的感知壓縮)。 已選原訂 22%,只有在深色頁面底上會衝到 16.1,因此降到 20%。
四道門檻對每一種底色都驗,不是只驗卡片:疊加層的意義就在於它要在所有表面上成立。
npm test 另有兩條程式碼守衛——元件不得再寫 hover:bg-accent/hover:bg-muted,
用了 state-layer 的元件不得同時寫 transition-colors(那個簡寫會蓋掉狀態層的過渡)。
background 簡寫會把 background-image 重設為 none,狀態層整個消失。
一律用 background-color——Tailwind 的 bg-* 產的就是 background-color,不受影響。
載入中:中性、三種手段各有位置
載入不是狀態語意——它不進提醒色的彩色家族,一律中性(--muted 骨架、
變暗遮罩)。原則同上:強度跟著持續時間走,而載入是「即將被取代的暫態」。
| 情境 | 手段 | 為什麼 |
|---|---|---|
| 首次載入、版面已知(清單、卡片、表格首載) | Skeleton:反映真實版面的灰塊 | 保留版面不跳動;讀者預先知道內容的形狀 |
| 已有內容、重新查詢(篩選、換頁) | 就地變暗+保留舊內容(aria-busy) | 舊資料仍可讀;已有資料再蓋骨架會閃 |
| 提交中(按鈕) | disabled+換圖示+改文案 | 按鈕沒有 loading 變體是刻意決策 |
骨架的形狀要反映真實版面(列數=每頁筆數、高度=實際行高)。
DataTable 的 loading 已內建這兩種長相的切換。
不要在已有資料的畫面上蓋骨架。每重查一次就閃一次骨架, 比「看著舊資料等新的」糟得多。
無障礙:載入狀態由容器宣告一次(aria-busy+role="status" 的 sr-only 文字),
骨架塊本身 aria-hidden——它不承載資訊,不要讓讀屏逐塊念灰色方塊。
另外載入中不是空——空狀態是「查完了沒有」,兩者要分得開。
保留色:琥珀=已改動未送出
--edit 這一組琥珀色只做一件事:標記「使用者改了,但還沒送出」。
不得挪作他用——不能拿來當高亮、當「新功能」標記、當任何其他強調。 一旦琥珀色在畫面上有第二種意思,「我改了什麼」這個最重要的訊號就被稀釋了。
理由與被否決的替代方案見 ADR-0002。
分類色票與狀態語意脫鉤
圖表的 8 色分類色票(--chart-1 … --chart-8)不參與狀態語意。
換一整套圖表色票,不會讓「紅=異常」跟著變。分類色票也不跟著色相主題走——
顏色跟資料實體走,同一個項目在所有圖表、所有主題下都是同一色。
脫鉤是雙向的:反過來,維度本身就是狀態時(狀態組成堆疊圖), 圖表必須沿用狀態色、不得照序取分類色——徽章教會使用者「已完成=綠」, 圖表不能把它變成藍的。完整判斷樹見圖表的配色策略。
為什麼分類色看起來比語意色「吵」
把〈設計 Token〉的兩個色票並排看,第一個反應通常是「這兩套像不同系統」。 這個落差是真的,而且是刻意的結果,不是缺陷。
語意系統用彩度編碼重要性:
danger 0.223 > warning 0.165 > edit 0.164 > success/info 0.148
> brand ≤0.151 > primary 0.040 > 中性 ≤0.013
語意色票裡絕大多數 token 是無彩的(背景 0.000、muted 0.007、border 0.013),
只有約十個帶彩度。所以那一頁讀起來是「一整頁灰,加幾個重點」——這正是後台介面該有的樣子。
分類色票沒有這個階序,八色等亮等飽和。在一個「彩度=重要性」的系統裡, 它看起來像八個東西同時宣稱自己最重要。
但等彩度對分類色是對的。 分類色是彼此平等的同儕——如果第 3 條序列比第 5 條飽和, 讀者會以為它比較重要。不等彩度會傳達一個不存在的階序。
所以兩邊都對,放在一起就會有落差:兩種色票的目的不同。 語意色回答「這件事有多重要」,分類色回答「這是哪一類」。 真正該管的不是拉近它們的觀感,而是確保分類色不會被誤讀成狀態色——見下一節。
分類色不得靠近任何狀態色
一條普通的資料序列如果剛好長得像 danger,讀者會以為那條線「有問題」。
這條守衛上一版只擋 danger,於是深色模式下 chart-5 與 warning 只差
ΔE00 7.3、chart-1 與 info 7.9——都低於色票自己的「實務上分不開」門檻 10,
而守衛全綠,因為它沒在看那兩個。現在對六個狀態色全驗,雙條件滿足其一即可:
- 色相拉開 ≥20°,或
- 感知距離拉開 ΔE00 ≥18
為什麼不是「只用 ΔE00 但把門檻拉高」:同色相但一深一淺的兩色 ΔE00 可以很大, 「紅色=異常」的聯想仍然成立。反過來只看色相也不夠——近中性色的色相角度沒有感知意義。
先前試過兩次「只加緊、不放寬」,兩次都是負收益(最差對 12.2 → 10.1)。 這一次加入狀態色保留區之後也一樣掉了(12.2/11.8 → 10.1/9.4,深色還出現 2 對低於 10)。
修法不是放寬色盲門檻——那條不動——而是放寬美感約束: 色相間距 30° → 22°、明度帶各放寬 0.02。結果最差對變成 13.1/11.3, 比加守衛之前還好。更寬的明度帶本來就對二色覺者有利(他們失去色相辨別、保留明度)。
門檻的優先序是寫死的:無障礙門檻不得為了美感放寬,反過來才是對的。
順序即安全順序
色票的排列不是色相環,是最遠點插入的順序:第 1 色錨在藍,之後每一色都是
「與已選色感知距離最遠」的那一個。副作用正是要的性質——取用端拿
--chart-1 … --chart-k,對任何 k 都是近似最佳的 k 色子集。
實測(含 protan/deutan 模擬的最小 ΔE00):
| 用到幾色 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|
| 最差一對 | 57 | 32 | 25 | 17 | 16 | 14 | 12 |
色票是優雅劣化的:系列愈多愈擠,但不會在某個 k 忽然斷掉。
淺色與深色是兩組獨立的值
這一點反直覺,但它是硬性的:一個顏色要同時對白底與深底都達 3:1,
OKLCH 的 L 只能落在 [0.49, 0.67]——寬度 0.17。八色共用同一組值,
就必然全部擠在中明度。
而二色覺者失去的正是色相辨別,保留的是明度。擠在中明度等於把唯一還能用的 維度放棄掉。所以淺深各生一組,各自吃滿自己的明度空間(L* 全距 31–37)。
上一版的 8 色裡有 6 色淺深共用同一 hex,L* 全距只有 26, 最差一對在紅綠色盲下 ΔE00 只有 2.6——實質同色。 當時的說法是「把最難分辨的藍↔紫拆到陣列兩端,讓相鄰系列永遠安全」, 但陣列距離只防相鄰,而相鄰只在堆疊長條與圓餅圖有意義。 這本書明訂不做圓餅圖,折線/散點/分組長條的任兩系列都會被並置比較—— 緩解措施沒有作用在它要保護的圖型上。
現在這一版由 npm run build:theme 生成、npm run verify:color 驗收,
兩兩距離不足會直接擋 PR。
--chart-1--chart-2--chart-3--chart-4--chart-5--chart-6--chart-7--chart-8對比要求
門檻取 WCAG 2.2 的嚴格值,不四捨五入:
| 用途 | 門檻 | 依據 |
|---|---|---|
| 一般文字(按鈕標籤、徽章文字) | 4.5:1 | 1.4.3 |
| 大字(≥18.66px 粗體/≥24px) | 3:1 | 1.4.3 |
| 圖表色塊、邊框、聚焦環 | 3:1 | 1.4.11 非文字對比 |
第三列常被寫成 4.5:1,那是錯的——而且是會造成傷害的錯:照 4.5:1 稽核, 會退掉一個其實完全合規的分類色票,然後有人為了「過檢」把它調成一組更難分辨的顏色。
淺色與深色各自驗一次,並且要驗「該模式裡最不利的表面」而不只是頁面底色。 聚焦環會出現在卡片與浮層上,只驗頁面底色會漏掉它們。
兩個容易漏的地方:
- 取整之後再驗。 浮點算出來合格、round 成 8-bit 之後掉到門檻以下, 是真實會發生的事,而且只在實機出現。
- 對實際的前景 token 驗,不是對理想的白。
--destructive-foreground是210 40% 98%(微藍的白)不是純白,差這 2% 就足以讓一個「算過的」值不過關。
這兩條都不是假設——本書的生成器兩條都犯過,是驗收腳本抓出來的。
顏色永遠不是唯一線索
這條規則貫穿整本書:
- 狀態徽章要有文字
- 變異數字要有箭頭 ▲▼ 與文字
- 選中狀態要有打勾
- 提示框要有圖示
灰階列印一份試試看——如果語意消失了,就是還沒做完。