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

深淺主題

兩種宿主鉤子,同時支援

.dark,
[data-theme="dark"] {
--background: 222 22% 8%;
/* … */
}
  • .dark class —— 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 不是淺色的反轉:

做法結果
機械式反轉亮度backgroundcard 撞在一起,介面扁平無層次
沿用同色相、壓低飽和度、建立表面抬升階沉靜的墨色底+清楚的層次

這套 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 家族(含 Buttonbrand 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-themeOKLCH 色相
石墨(預設)graphite265°(無品牌色--brand 鏡射 --primary
靛藍indigo272°
藍紫violet292°
紫晶amethyst305°
青玉teal195°
苔綠moss135°

石墨那一組刻意沒有品牌色——它的定位是「不挑主題時等同現況」,而現況本來就沒有 --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.mjsTHEMES, 跑 npm run build:theme 生成,再跑 npm run verify:color 驗收—— 其中一條會擋「brand 與最近的狀態色距離不足」。

列印是第三種主題

大多數團隊只想到淺/深兩種。後台系統還有第三種:印出來的那一份

模式 → 列印與匯出