深淺主題
兩種宿主鉤子,同時支援
.dark,
[data-theme="dark"] {
--background: 222 22% 8%;
/* … */
}
.darkclass —— Tailwind / shadcn 生態的慣例[data-theme="dark"]屬性 —— Docusaurus、多數文件站與部分後台框架的慣例
兩個都支援的成本只是 CSS 多一個選擇器,但少支援一個,
這套 token 就少掉一整類可以用它的宿主。你正在讀的這個文件站就是靠 [data-theme] 那條。
切換要掛在 documentElement
document.documentElement.classList.toggle("dark", isDark);
document.documentElement.setAttribute("data-theme", isDark ? "dark" : "light");
把主題 class 切在 <html> 上。
不要只切在某個 wrapper <div> 上。Dialog / Select / Tooltip
走 portal 掛到 <body>,wrapper 內的 class 它們抓不到——
結果是浮層永遠是淺色,而且很難查。
深色是設計出來的,不是算出來的
深色 token 不是淺色的反轉:
| 做法 | 結果 |
|---|---|
| 機械式反轉亮度 | background 與 card 撞在一起,介面扁平無層次 |
| 沿用同色相、壓低飽和度、建立表面抬升階 | 沉靜的墨色底+清楚的層次 |
這套 token 的深色沿用淺色的冷靛藍色相,飽和度從 84% 壓到 16–22%, 並建立五級表面抬升。每個值都對深色實際背景重新驗過對比,不是沿用淺色的數字。
色票研究時常用的比較背景(例如 #020817)和實際頁面背景(#101319)亮度不同。
背景換了,對比結果就要重跑。這是實際踩過的坑。
圖表色票是兩組獨立的值,不是「有幾色不一樣」
分類色票在淺色背景上安全,不代表在深色背景上安全——但真正的重點不是「驗兩次」, 是不要共用。
一個顏色要同時對白底與深底都達 3:1,OKLCH 的 L 只能落在 [0.49, 0.67],寬度 0.17。
八色共用一組值,就必然全部擠在中明度;而二色覺者失去的正是色相辨別、保留的是明度。
共用等於把唯一還能用的維度放棄掉。
import { chartColors } from "@dooping/tokens";
chartColors("light"); // 8 色,L* 落在 30–62
chartColors("dark"); // 另外 8 色,L* 落在 49–86 —— 沒有任何一色與淺色相同
npm run verify:color 有一條專門擋這件事:只要淺深出現共用值就不合格。
本書上一版 8 色裡共用了 6 色,就是這樣壞掉的,見 色彩語意。
多色相主題
深/淺是明度的兩種模式,色相主題是另一個維度。兩者正交,可以任意組合。
<html data-theme="dark" data-color-theme="indigo">
data-theme/.dark—— 明暗,既有機制不變data-color-theme—— 色相,不設就是預設主題(石墨),輸出與沒有這個功能時完全一樣
主題影響哪些 token
每組主題每個模式共 16 個,分兩類:
| 類別 | Token | 怎麼變 |
|---|---|---|
| 主題色(4) | --brand、--brand-foreground、--brand-subtle、--brand-subtle-foreground | 完整重新生成(對比反解) |
| 帶色調的中性色(12) | --background、--card、--popover、--muted、--secondary、--accent、--border、--input、--field-border、--field-editable、--field-readonly、--muted-foreground | 只轉色相,L 與 chroma 不動 |
與主題無關的:--primary(中性近黑,承擔多數控制項)、--ring(聚焦環,
中性——曾在 v0.4.0 吃主題色相,v0.7.0 依 ADR-0007 改回)、全部狀態色、
全部圖表色票、提醒視窗的 --{狀態}-subtle。
色相預算:主題色相只進識別層
「主題影響哪些 token」的白名單不是湊出來的,背後是一條可以背下來的規則 (決策正本與量測理由見 ADR-0007):
| 介面層 | 吃主題色相? | 載體 |
|---|---|---|
| Layout 大區塊背景風格色調 | ✅(只轉色相) | 12 個帶色調中性色 |
| 選單/選中導覽項/品牌強調 | ✅ | --brand 家族(含 Button 的 brand variant——僅限非提交型入口) |
| 動作(確認/送出/儲存) | ❌ | --primary 中性近黑 |
| 互動(hover/pressed/已選) | ❌ | currentColor 疊加,機制無色相 |
| 聚焦環 | ❌ | --ring 中性,全主題一致 |
| 狀態/提醒色 | ❌ | 語意固定,見提醒色辭典 |
| 資料(圖表 8 色) | ❌ | 全主題共用 |
聚焦環改回中性的直接理由:欄位提醒色(琥珀=已改動、紅=有問題)與聚焦環 會出現在同一個輸入框上。ring 帶主題色相時,這個組合每換一次主題就換一副長相; 中性之後「彩色=語意、中性=焦點」在所有主題下恆成立,切主題不會誤讀 ring 的用意。
中性色為什麼要帶主題色相
這些 token 的 chroma 只有 0.007–0.023——單看一格分不出來,但它們是畫面上面積最大的那 60%。
中性色如果固定在冷藍(248–267°)而主色是青玉或苔綠,介面會有一種說不上來的 「兩套系統拼裝」感:主色是暖綠,它坐的表面卻是冷藍。
青玉 muted #f0f6f6 border #deeaea
苔綠 muted #f3f6f1 border #e4e9e1
紫晶 muted #f5f3f8 border #e9e5ee
只轉色相、L 與 chroma 一律不動,所以明暗層次、表面抬升階、對比關係全部原封不動。
唯一的例外是 --muted-foreground:它會落在 --muted 上,轉完色相後對比會位移
(實測最差掉到 4.25:1),因此那一格是對「該主題的 muted」反解到 4.5:1 產生的,
不是單純轉色相。
這也是提醒視窗能自動繼承主題的前提——淡底疊在這些表面上,一度色相都不用彎。
六組預設主題
| 名稱 | data-color-theme | OKLCH 色相 |
|---|---|---|
| 石墨(預設) | graphite | 265°(無品牌色,--brand 鏡射 --primary) |
| 靛藍 | indigo | 272° |
| 藍紫 | violet | 292° |
| 紫晶 | amethyst | 305° |
| 青玉 | teal | 195° |
| 苔綠 | moss | 135° |
石墨那一組刻意沒有品牌色——它的定位是「不挑主題時等同現況」,而現況本來就沒有
--brand。硬擠出一個近中性的填色會撞進停用的視覺位置,見
色彩語意 → brand 的邊界。
色相是被狀態色瓜分過的
新增主題時色相不能亂選——狀態色已經佔掉幾段,主色相如果落在附近, 使用者會停止把那個顏色讀成狀態。
danger 17.7° destructive 25.3° warning 70.6°
edit 83.9° success 162.4° info 238.1°
info 在 238° 這件事影響最大:企業系統最常用的藍色主色區被狀態色卡住了。
想用傳統企業藍(約 245°)就得先把 info 移開,那是會波及既有畫面的決定。
新增主題請改 packages/tokens/scripts/generate-theme.mjs 的 THEMES,
跑 npm run build:theme 生成,再跑 npm run verify:color 驗收——
其中一條會擋「brand 與最近的狀態色距離不足」。
列印是第三種主題
大多數團隊只想到淺/深兩種。後台系統還有第三種:印出來的那一份。
見 模式 → 列印與匯出。