暗色主題最常見的視覺失敗有三個:用純黑當背景、用純白當文字、以及把亮色主題的陰影原封不動搬過來。三者都有明確的機制解釋與可執行的替代方案。

不要用 #000000 當背景

純黑背景配亮色文字會產生兩個實際問題。其一是光暈(halation):高對比邊緣在暗適應的眼睛上會擴散,白字在純黑上看起來像在發光、筆畫糊在一起,散光使用者尤其明顯。其二是沒有向下的空間:純黑之下沒有更暗的顏色,所有需要「比背景更低一階」的元素(凹陷輸入框、頁面底層、遮罩)都無處可去。

Material 的暗色指引把基準表面訂在 #121212 而非純黑,正是為了保留下探空間並降低光暈。實務上 #0e1013#16181c 這個區間都合理,選擇取決於品牌色相——帶一點色相偏移(例如藍調的 #0e1013)會比中性灰更有質感,而且能與品牌色協調。

文字同理:純白 #fff 在深底上會過亮。web.dev 的建議是改用略暗的白,例如 rgb(250, 250, 250)。實務上正文用 #e6e8eb 這個量級,既舒服又足夠對比。

對比要求:具體數字

WCAG 2.2 的 SC 1.4.3(Contrast Minimum,AA 級):

  • 一般文字:至少 4.5:1
  • 大字:至少 3:1。大字定義為 18pt 以上,或 14pt 以上的粗體;換算約為 24px18.5px
  • 例外:停用狀態的介面元件、純裝飾、不可見文字、以及logo 或品牌名稱中的文字沒有對比要求

另外 SC 1.4.11(Non-text Contrast)要求介面元件與必要圖形至少 3:1——這條管的是輸入框邊界、核取方塊的框線、圖示、圖表的資料線。暗色主題最常違反的就是這一條:邊框顏色從亮色主題等比換算過來,往往只剩 1.5:1。

兩套主題都要各自量測。 亮色過了不代表暗色過,這是必須寫進檢查清單的一句話。CSS 端可以用 color-contrast() 之類的新功能輔助,但目前支援尚不穩定,實務上仍以工具量測為準(DevTools 的對比檢查、axe、Lighthouse)。

高程:暗色靠明度,不靠陰影

在亮色主題,「浮起來」是靠陰影表達的——白卡片投影在灰底上。同一套陰影搬到暗色主題會幾乎看不見,因為深色背景上的深色陰影沒有對比。結果是所有卡片、下拉、對話框在暗色下全部塌成同一平面。

Material 的解法是用疊層取代陰影:表面愈高,疊在 #121212 上的白色半透明疊層愈強,透明度從 0dp 的 0% 到 24dp 的 16%。也就是說暗色主題的層次是靠背景變亮表達的。

翻譯成 CSS token:

:root {
  color-scheme: light dark;
 
  /* 亮色靠陰影,暗色靠明度 */
  --surface-0: light-dark(#ffffff, #121212);  /* 頁面底 */
  --surface-1: light-dark(#ffffff, #1c1c1e);  /* 卡片 */
  --surface-2: light-dark(#ffffff, #232326);  /* 浮動面板、下拉 */
  --surface-3: light-dark(#ffffff, #2a2a2e);  /* 對話框、最上層 */
 
  --shadow-1: light-dark(0 1px 2px rgb(0 0 0 / .08), none);
  --shadow-2: light-dark(0 4px 12px rgb(0 0 0 / .12), 0 4px 12px rgb(0 0 0 / .5));
}
 
.card    { background: var(--surface-1); box-shadow: var(--shadow-1); }
.dropdown{ background: var(--surface-2); box-shadow: var(--shadow-2); }

暗色的陰影不是完全不用,而是用途改變:它不再表達高度,而是用來把浮動元素與底下的內容分開(避免視覺黏在一起),所以要更黑、更擴散。上面 --shadow-2 在暗色用 .5 的黑而非 .12,就是這個道理。

如果要用疊層的做法而不是手調每一階,可以用 color-mix() 讓層次由公式產生:

:root { --elevation-tint: light-dark(transparent, #ffffff); }
.surface-2 {
  background: color-mix(in srgb, var(--elevation-tint) 8%, var(--surface-0));
}

半透明疊層的陷阱

rgba(0, 0, 0, 0.08) 這種寫死黑色的疊層在暗色主題會消失(黑疊黑),rgba(255,255,255,0.08) 在亮色主題會消失(白疊白)。這類值在既有 CSS 裡通常散落各處:hover 底色、分隔線、遮罩、skeleton loading。

解法是把「疊層顏色」本身做成 token:

:root {
  --overlay-hover:  light-dark(rgb(0 0 0 / .05), rgb(255 255 255 / .07));
  --overlay-active: light-dark(rgb(0 0 0 / .09), rgb(255 255 255 / .12));
  --scrim:          light-dark(rgb(0 0 0 / .4),  rgb(0 0 0 / .65));  /* 對話框遮罩兩套都是黑,但暗色要更黑 */
}

注意 --scrim 兩套都是黑色但濃度不同——遮罩的目的是壓暗底層內容,暗色主題下底層本來就暗,需要更濃才能造出足夠的分離感。

品牌色與語意色的處理

飽和度高的品牌色在深底上會產生視覺震盪,而且多半過不了 4.5:1。Material 的指引明講:暗色主題應避免使用飽和色,改用去飽和版本作為更易讀的替代。

三個可執行的調整:

  1. 主色取色階的淺端:亮色用 500,暗色用 300 或 200。
  2. 降飽和 10–25%:保留色相辨識度,去掉震盪。
  3. 疊在主色上的文字要反轉:亮色主題的按鈕是「深藍底白字」,暗色主題若主色變成淺藍,文字就必須變成深色(--accent-text: light-dark(#fff, #0e1013))。忘記這一步會得到「淺藍底白字」,對比只剩 1.8:1。

錯誤/警告色也一樣。純紅 #ff0000 在暗底上刺眼且對比不足;#f87171 這個量級才合適。

焦點環:暗色最容易壞掉的地方

鍵盤焦點環在暗色主題最常被做壞,因為它通常是用單一顏色寫死的。焦點指示器屬於非文字內容,需達 3:1。最穩的做法是雙色描邊,讓它在任何背景上都可見:

:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px var(--focus-ring-halo);   /* 反差色的外圈 */
}
:root {
  --focus-ring:      light-dark(#1a5fd0, #7fb0ff);
  --focus-ring-halo: light-dark(rgb(255 255 255 / .9), rgb(0 0 0 / .9));
}

千萬不要在暗色主題把焦點環拿掉。這是無障礙的硬性要求,不是視覺偏好。

其他常被漏掉的細節

  • ::selection 的預設反白色在暗色下可能造成文字看不見,要一併定義。
  • ::placeholder 常常在暗色主題掉到 2:1 以下。它雖然不是必要文字,但過低會讓表單無法使用。
  • 程式碼區塊的語法高亮是一整套獨立的顏色系統,通常需要換一份主題(例如亮色用 GitHub Light、暗色用 GitHub Dark),不能只反轉背景。
  • 表格的斑馬紋在暗色下要比亮色更細微,否則會變成明顯的條紋。
  • autofill 的黃色底由瀏覽器繪製,設定 color-scheme 才會跟著變暗,見 原生控件與捲軸

下一步:圖片、圖示、logo 與 media