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

深淺主題

兩種宿主鉤子,同時支援​

.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 不是淺色的反轉:

做法結果
機械式反轉亮度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​

每組主題每個模式共 22 個,分三類:

類別Token怎麼變
主題色(4)--brand、--brand-foreground、--brand-subtle、--brand-subtle-foreground完整重新生成(對比反解)
帶色調的中性色(14)--background、--card、--popover、--muted、--secondary、--accent、--border、--input、--field-border、--field-editable、--field-readonly、--muted-foreground、--sidebar、--sidebar-border只轉色相,L 與 chroma 不動
側欄別名(4)--sidebar-primary、--sidebar-primary-foreground、--sidebar-accent、--sidebar-accent-foreground=brand 家族的字面值複製(生成器保證同值,verify:color 的別名守衛盯住)

與主題無關的:--primary(中性近黑,承擔多數控制項)、--ring(聚焦環, 中性——曾在 v0.4.0 吃主題色相,v0.7.0 改回:聚焦環要在六個主題最亮的表面上都過 3:1,跟著色相走做不到)、 --sidebar-ring(≡ ring,同一條規則)、--sidebar-foreground(≡ foreground)、 全部狀態色、全部圖表色票、提醒視窗的 --{狀態}-subtle。

色相預算:主題色相只進識別層​

「主題影響哪些 token」的白名單不是湊出來的,背後是一條可以背下來的規則 (量測理由:主題只換色相、不動明度與對比關係,所以無障礙在這個決定上是中立的):

介面層吃主題色相?載體
Layout 大區塊背景風格色調✅(只轉色相)14 個帶色調中性色(含 --sidebar、--sidebar-border)
選單/選中導覽項/品牌強調✅--brand 家族(含 Button 的 brand variant——僅限一頁一顆的非提交型入口,這條沒有守衛,人工:守門人,收新頁型或審 PR 時;--sidebar-primary/--sidebar-accent 是它的別名)
動作(確認/送出/儲存)❌--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.mjs 的 THEMES, 跑 npm run build:theme 生成,再跑 npm run verify:color 驗收—— 其中一條會擋「brand 與最近的狀態色距離不足」。

列印是第三種主題​

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

見 模式 → 列印與匯出。