跳至主要内容
💬 回報這一頁

色彩語意

這一章講的不是色票,是意思。

Token 的名字說的是「這個顏色代表什麼」,不是「這是什麼顏色」。 所以沒有 --blue-500,只有 --primary--danger--field-editable。 換品牌色時改的是值,不是任何一行使用它的程式碼。理由見 ADR-0001

四組語意,職責不重疊

1. 表面與文字(結構)

Token意思
--background / --foreground頁面底色與主要文字
--card / --card-foreground卡片表面
--popover / --popover-foreground浮層表面(下拉、泡泡、篩選面板)
--muted / --muted-foreground弱化表面與次要文字
--accent / --accent-foregroundhover/被指向的表面
--border / --input / --ring分隔線、控制項邊框、鍵盤聚焦環

深色模式下這五種表面構成抬升層次background < card < popover < muted < border。 深色不是把淺色反轉——反轉出來的介面會扁平到分不出層。

2. 動作(控制項)

Token用在哪
--primary主要動作(中性近黑,與色相主題無關
--secondary次要動作
--brand色相主題色:一頁一顆的關鍵動作、品牌強調
--brand-subtle主題色淡底:選中的導覽項、分頁底線區
--destructive破壞性動作的控制項(刪除鈕)
primarybrand 為什麼分開

品牌色為了視覺張力通常偏亮偏飽和,而後台畫面上按鈕很多。 兩者共用一個 token,等於讓高飽和色的出現面積失控—— 色彩疲勞管的是面積與頻率,不是色相的種類數。

所以 --primary 維持中性近黑,承擔絕大多數控制項;--brand 吃色相主題, 只給一頁一顆的關鍵動作。換主題時介面的重心不會整個變吵。 見 深淺主題 → 多色相主題

3. 狀態(訊息與資料)

Token意思
--success良好/已完成/通過
--warning需要注意,但不阻擋
--info中性提示/補充說明
--danger異常/錯誤/不合格

每個提醒色的完整定義、使用場景、反例與擴充程序,見提醒色辭典

destructivedanger 為什麼分開

一個是「按下去會刪東西」,一個是「這筆資料有問題」。 同一個畫面上兩者常常同時出現(一列紅色的異常資料,右邊一顆紅色的刪除鈕)。 混用會讓紅色失去意義——使用者不再知道紅色到底在警告什麼。

4. 欄位(可編輯 vs 唯讀)

Token意思
--field-editable--field-border這一格你可以改
--field-readonly這一格是算出來的/唯讀
--edit / --edit-bg / --edit-foreground已改動未送出(保留色)

欄位色與 edit 的完整定義同樣收在提醒色辭典

欄位只有兩種語意

可編輯

1,500,000

唯讀/計算值

1,380,000

已改動未送出

1,650,000
✅ 這樣做

欄位只分兩種:可編輯、唯讀。加上「已改動未送出」總共三種狀態, 使用者掃一眼就懂。

🚫 不要這樣

不要做「輸入/假設/公式」三色。那是試算表的儲存格慣例, 對網頁表單的使用者不成立——他不在乎這格背後是不是公式,他只在乎能不能改。 (這是實際踩過的坑,見 ADR-0002

brand 的邊界:不要放在確認按鈕上

--brand--primary 分開之後還有一個問題沒解決:brand 該出現在哪裡。

答案是不要出現在確認/送出/儲存這類提交性動作上。理由與 destructivedanger 分家完全相同——兩者職責不同:

職責
--brand識別:這是誰的產品
確認按鈕指示可供性:按下去會提交

讓一個 token 同時做兩件事就會產生違和。而且色相帶著既成慣例:

色相慣例放在確認按鈕上
通行、完成對齊
系統預設動作(macOS/Windows 的預設鈕都是藍)對齊
紫/洋紅沒有動作慣例讀成裝飾,不像功能控制項
停止直接衝突(已被警報色域擋掉)
停用反慣例
brandprimary
logo 區、品牌強調確認、送出、儲存、套用
「開始新流程」這類非提交型入口一般操作
選中的導覽項(走 --brand-subtle取消、返回

提交性動作在任何主題下都用 --primary,色相就永遠不會跟動作語意打架。 這也是六個色相全部保留的理由——違和來自把 brand 放錯位置,不是色相不能用。

近中性色不能拿來做實色填底

Button 的停用態是 disabled:opacity-50,所以「停用」的實際外觀就是 --primary 以 50% 疊在背景上。一個接近中性的主題色做成填色按鈕,會正好撞進那個位置。

本書的預設主題「石墨」先前就是這樣:照公式生成一個 chroma 0.030 的灰藍, 量出來距停用外觀只有 ΔE00 8.5(低於 10 就是實務上分不開)——按鈕看起來像停用的。

修法不是把它加彩,而是承認它沒有品牌色:石墨的 --brand 直接鏡射 --primarynpm 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
--danger0.223
--destructive0.208
--warning / --edit0.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 高強度出現頻率一高,色彩疲勞的預算會瞬間爆掉—— 疲勞管的是高飽和色的面積 × 頻率,不是色相的種類數。提醒視窗的關鍵色與圖表的重點色 共用同一份預算。

淡底不能用「實色壓 10%」做

bg-danger/10 這種寫法的對比完全不可控,因為結果取決於底下是什麼表面。 本書上一版就是這樣做的,實測四種變體在淺色模式的文字對比只有 1.97–3.98:1, 全部低於 4.5——而且不會有任何東西報錯。

現在改成生成的實色 token,文字對底色反解到 4.5:1,npm run verify:color 逐一驗。

四種淡底必須彼此分得開

--{狀態}-subtle 刻意不做 harmonization(不往主題色相偏)。實測往主題拉 12°, 藍紫系主題的 warning-subtledanger-subtle 會收斂到 ΔE00 8.8—— 琥珀和紅都被拉成粉橘,「注意」和「錯誤」看起來變成同一種。

提醒視窗的整體感靠兩件事,都不是彎色相:四種共用同一條構成規則 (下一節),以及它們坐在帶主題色相的中性表面上。

「同一層」要等量,不是等參數

淡底的生成參數是染色量ΔE00(淡底, 卡片表面)),不是彩度。

上一版是「固定明度 + 固定彩度」——數字很整齊,感知不整齊。原因是 sRGB 色域不是色相對稱的:在很亮的地方,綠與黃能撐住的彩度遠高於藍與紅, 於是 infodanger 被色域裁掉、success 沒有。實測結果:

infodangerwarningsuccess最強/最弱
舊(固定彩度)11.012.614.118.51.69×
現在(固定染色量)14.515.115.014.81.04×

舊的那一列順序是反的:綠色的「已全部完成」比紅色的「無法確認」還搶眼。 四種提示疊成一欄時這件事直接看得到,而且叫不出名字——這正是「突兀」的來源之一。

規則是兩段的:四種共用一個明度(所以看起來是同一家人), 各色相的彩度則反解到同一個染色量。彩度不能也固定,因為藍色在深色中性底上 本質上比較不顯眼,深色下 info 需要約 3.3 倍的彩度才追得上—— 那個差距是對感知不對稱的補償,不是不一致。

npm run verify:color 有一條門檻:四種染色量的最大/最小比值不得超過 1.3

左粗邊用文字色,不用實色

低強度的左粗邊是 border-l-current——與內文同色(--{狀態}-subtle-foreground), 不是全飽和的狀態色。兩個理由都是量出來的:

  1. 全飽和邊對自己的淡底只有 1.60–5.43:1,八組(四變體 × 兩模式)裡有四組低於 3:1 的非文字門檻(1.4.11)。改用文字色之後全部是 4.50–4.54:1—— 因為那個色本來就是對淡底反解到 4.5:1 的,所以「相等」是構造上保證的。
  2. 淡底的彩度約 0.03–0.08,全飽和邊卻是 0.134–0.223(3–6 倍)。 一個元件內部跳這麼大,就是使用者說的「突兀」。文字色的彩度約 0.100,跳幅收到 2 倍內。

欄位狀態:三層各司其職

一格欄位可以同時是「被聚焦」「改過沒送」「不合格」。這三件事走三個不同的通道, 疊起來互不干涉:

表達什麼用什麼
聚焦環鍵盤焦點在這裡(控制項語意)--ring恆定不隨狀態變色
邊框+底色這格的狀態(狀態語意)三選一,有優先序
圖示+文字為什麼aria-invalid + aria-describedby
狀態邊框底色優先序
唯讀/計算值--field-readonly
可編輯--field-border--field-editable0
已改動未送出--edit--edit-bg1
不合格--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
已改動未送出直到送出還沒存狀態色琥珀(見下一節)
不合格直到修正這個值不能用狀態色紅

四個判斷值得單獨講:

  1. 三個互動狀態全部走中性,不吃主題色。 高飽和色的出現面積是有預算的 (見上面的面積層)。hover 是滑過就沒的瞬間回饋,替它上主題色是最貴的用法。
  2. focus 走「環」這條獨立通道,不碰底色。 所以它能與任何狀態疊加而不打架—— 不合格+聚焦、已選+聚焦都成立。
  3. 停用是「移除可供性」,不是「增加訊息」,所以是降低而不是加色。 反過來說,任何常駐的半透明控制項都會被讀成不能按。
  4. 已選的列不得再用弱化文字。 20% 疊加之後 --muted-foreground 掉到 2.77:1(淺)/2.96:1(深)。這不是調數值能解的——已選的列在語意上就是 被強調的,它的次要文字不該繼續弱化。規則:已選的列把 --muted-foreground 整個重新定義成 --foreground

狀態層是「疊加」,不是「換一組底色」

改版前 hover 用 --accent、已選用 --muted。問題是這三個 token (--accent--muted--secondary)目前是同一個值——於是被指到的列、 斑馬列、唯讀區三者同色。實測:

淺色 ΔE00深色 ΔE00
一般 → hover1.62.5
hover → 已選1.62.3

ΔE00 低於 2 幾乎看不見。使用者分不出自己指在哪一列,也分不出 hover 與已選。

根因不是顏色選錯,是機制選錯:獨立的實色 token 是「取代」而不是「疊加」, 所以它無法與元件已有的底色組合。斑馬列本來就是 --muted,hover 換成同值的 --accent 等於什麼都沒做。

改成疊一層之後,「深了一階」這件事在每一種底色上都成立:

底色hover Δ已選 Δ
頁面底3.010.8
卡片3.010.8
--muted(斑馬列)3.010.7
浮層3.010.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-accenthover:bg-muted, 用了 state-layer 的元件不得同時寫 transition-colors(那個簡寫會蓋掉狀態層的過渡)。

唯一的取用端陷阱

background 簡寫會把 background-image 重設為 none,狀態層整個消失。 一律用 background-color——Tailwind 的 bg-* 產的就是 background-color,不受影響。

載入中:中性、三種手段各有位置

載入不是狀態語意——它不進提醒色的彩色家族,一律中性(--muted 骨架、 變暗遮罩)。原則同上:強度跟著持續時間走,而載入是「即將被取代的暫態」。

情境手段為什麼
首次載入、版面已知(清單、卡片、表格首載)Skeleton:反映真實版面的灰塊保留版面不跳動;讀者預先知道內容的形狀
已有內容、重新查詢(篩選、換頁)就地變暗+保留舊內容aria-busy舊資料仍可讀;已有資料再蓋骨架會閃
提交中(按鈕)disabled+換圖示+改文案按鈕沒有 loading 變體是刻意決策
✅ 這樣做

骨架的形狀要反映真實版面(列數=每頁筆數、高度=實際行高)。 DataTableloading 已內建這兩種長相的切換。

🚫 不要這樣

不要在已有資料的畫面上蓋骨架。每重查一次就閃一次骨架, 比「看著舊資料等新的」糟得多。

無障礙:載入狀態由容器宣告一次(aria-busyrole="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-5warning 只差 ΔE00 7.3chart-1info 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):

用到幾色2345678
最差一對57322517161412

色票是優雅劣化的:系列愈多愈擠,但不會在某個 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。

分類色票(8 色)
--chart-1
--chart-2
--chart-3
--chart-4
--chart-5
--chart-6
--chart-7
--chart-8

對比要求

門檻取 WCAG 2.2 的嚴格值,不四捨五入

用途門檻依據
一般文字(按鈕標籤、徽章文字)4.5:11.4.3
大字(≥18.66px 粗體/≥24px)3:11.4.3
圖表色塊、邊框、聚焦環3:11.4.11 非文字對比

第三列常被寫成 4.5:1,那是錯的——而且是會造成傷害的錯:照 4.5:1 稽核, 會退掉一個其實完全合規的分類色票,然後有人為了「過檢」把它調成一組更難分辨的顏色。

淺色與深色各自驗一次,並且要驗「該模式裡最不利的表面」而不只是頁面底色。 聚焦環會出現在卡片與浮層上,只驗頁面底色會漏掉它們。

對比要在實際會被畫出來的顏色上驗

兩個容易漏的地方:

  1. 取整之後再驗。 浮點算出來合格、round 成 8-bit 之後掉到門檻以下, 是真實會發生的事,而且只在實機出現。
  2. 對實際的前景 token 驗,不是對理想的白。 --destructive-foreground210 40% 98%(微藍的白)不是純白,差這 2% 就足以讓一個「算過的」值不過關。

這兩條都不是假設——本書的生成器兩條都犯過,是驗收腳本抓出來的。

顏色永遠不是唯一線索

這條規則貫穿整本書:

  • 狀態徽章要有文字
  • 變異數字要有箭頭 ▲▼ 與文字
  • 選中狀態要有打勾
  • 提示框要有圖示

灰階列印一份試試看——如果語意消失了,就是還沒做完。