暗色主題最常見的視覺失敗有三個:用純黑當背景、用純白當文字、以及把亮色主題的陰影原封不動搬過來。三者都有明確的機制解釋與可執行的替代方案。
不要用 #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 以上的粗體;換算約為 24px 與 18.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 的指引明講:暗色主題應避免使用飽和色,改用去飽和版本作為更易讀的替代。
三個可執行的調整:
- 主色取色階的淺端:亮色用 500,暗色用 300 或 200。
- 降飽和 10–25%:保留色相辨識度,去掉震盪。
- 疊在主色上的文字要反轉:亮色主題的按鈕是「深藍底白字」,暗色主題若主色變成淺藍,文字就必須變成深色(
--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。