「頁面換成暗色了,但輸入框還是白的、捲軸還是亮的、下拉選單打開是白底」——這一類 bug 的成因只有一個:沒有設 color-scheme。UA 繪製的元件不看你的 CSS 變數,它只看這個屬性。這一篇處理所有由瀏覽器而非你繪製的東西。

color-scheme 涵蓋的範圍

設定 color-scheme 後,UA 會自動處理:畫布底色、捲軸、表單控件(inputselecttextareabutton 的預設外觀)、原生下拉清單、日期/顏色選擇器的彈出面板、拼字檢查底線、以及 autofill 的背景色。

這是一條非常划算的規則:一行宣告換掉一整批你根本無法用 CSS 完整控制的介面。 日期選擇器的彈出面板、<select> 展開的原生清單、autofill 的黃底,這些在多數瀏覽器上都無法用 CSS 完全覆寫,只能靠 color-scheme

:root { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark;  }

手動切換時務必連 color-scheme 一起改(見 三態切換器),否則會出現「頁面是暗的,但下拉選單是亮的」這種只有一半的畫面。

表單控件的細部調整

color-scheme 給的是合理預設,但品牌化仍需要幾個屬性:

:root {
  accent-color: var(--accent);          /* checkbox、radio、range、progress 的主色 */
  caret-color:  var(--text-primary);    /* 輸入游標 */
}
 
input, textarea, select {
  background: var(--bg-surface);
  color: var(--text-primary);
  border: 1px solid var(--border-subtle);
}
 
::placeholder { color: var(--text-muted); opacity: 1; }   /* Firefox 預設會降透明度 */
 
::selection {
  background: var(--accent);
  color: var(--accent-text);
}

accent-color 特別值得用:它讓核取方塊與滑桿套上品牌色,同時保留原生的可及性與觸控行為。瀏覽器會自動計算勾勾的對比色,所以你不需要另外處理暗色——但如果你的 accent 在暗色主題變成淺色,記得檢查勾勾是否仍清晰

::placeholder 在 Firefox 有預設透明度,不寫 opacity: 1 會讓它在暗色下更淡、掉到 2:1 以下。

autofill 的背景色是老問題。Chromium 用 -webkit-autofill 偽類且無法用一般 background-color 覆寫(會被 UA 樣式蓋掉)。設好 color-scheme 之後多數情況已經正確;若仍需覆寫,通用的 hack 是用超長的 inset shadow:

input:-webkit-autofill {
  -webkit-text-fill-color: var(--text-primary);
  box-shadow: 0 0 0 1000px var(--bg-surface) inset;
  transition: background-color 5000s;   /* 拖延 UA 的背景動畫 */
}

這是 hack,不是標準行為,升級瀏覽器時要重測。標示為瀏覽器相依。

捲軸

標準做法是 scrollbar-colorscrollbar-width

:root {
  scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
  --scrollbar-thumb: light-dark(#c4c9d0, #3a3f47);
  --scrollbar-track: light-dark(#f1f3f5, #17191d);
}

三個注意事項:

  1. 多數情況下不要動它。 設了 color-scheme 之後捲軸已經會跟著換色,而且是各平台原生的樣子。自訂捲軸經常做出比預設更難用的東西(太細、對比不足、失去 hover 放大)。
  2. 不要對捲軸顏色做動畫。 Chrome 的指引明確提到要避免,會造成明顯的效能問題。
  3. macOS 的覆蓋式捲軸(overlay scrollbar)在設定 scrollbar-color 後可能被改成常駐佔位的樣式,導致版面寬度改變。要搭配 scrollbar-width 一起考量。

若仍需支援不認得 scrollbar-color 的舊瀏覽器,用 @supports not 包住 -webkit- 版本,避免兩套規則互相干擾:

@supports not (scrollbar-color: auto) {
  ::-webkit-scrollbar { width: 12px; }
  ::-webkit-scrollbar-track { background: var(--scrollbar-track); }
  ::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 6px; }
}

forced-colors:與暗色模式無關的另一件事

Windows 高對比模式(現稱 Contrast Themes)不是暗色模式的一種,它是完全不同的機制:使用者選定一組有限的色盤,瀏覽器強制套用到所有網站,並透過 CSS 系統顏色關鍵字(CanvasCanvasTextLinkTextButtonFaceButtonTextHighlight 等)把色盤提供給作者。

在這個模式下,你的 --bg-surface 幾乎全部被忽略。這是刻意的,也是正確的——使用者的對比需求優先於你的設計。你要做的不是對抗它,而是確保介面在被強制換色後仍然可用

@media (forced-colors: active) {
  /* 用背景色表達的狀態會消失,改用邊框 */
  .badge { border: 1px solid CanvasText; }
 
  /* 焦點環必須保留 */
  :focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
 
  /* 用 background-image 畫的圖示會不見,改用系統色 */
  .icon { forced-color-adjust: none; color: CanvasText; }
}

forced-color-adjust: none 是強力工具,MDN 的說明是它應該只用於支援使用者的顏色與對比需求(例如 UA 的優化反而讓體驗變差時),不是用來保住你的品牌色。過度使用等於把使用者明確表達的無障礙需求關掉。合理的用例是彩色的資料圖表、色票、品牌 logo——這些反而是把顏色抹掉會失去資訊的地方。

forced-colors 的檢查清單很短,但漏掉會直接導致不可用:

  • 用純背景色區分的狀態(選取中的分頁、啟用的按鈕)在強制色下會全部一樣 → 加邊框或加文字標示。
  • background-image 或 CSS 畫的圖示會消失 → 改 inline SVG(會被賦予系統色)或明確處理。
  • 透明度與陰影會被移除 → 不要用它們承載唯一的資訊。
  • 自訂的核取方塊(用 appearance: none 重畫的)在強制色下經常變成看不見的方塊 → 這是把原生控件重畫的隱藏成本。

測試方式:Windows 上開「設定 → 協助工具 → 對比佈景主題」,或在 Chrome/Edge DevTools 的 Rendering 面板模擬 forced-colors: active。Firefox 也支援相關的模擬。

prefers-contrast:使用者想要更高對比

與 forced-colors 相鄰但不同的是 prefers-contrast,它表示使用者要求更高或更低的對比,但沒有強制色盤。可以用它把邊框加深、把 muted 文字拉亮:

@media (prefers-contrast: more) {
  :root {
    --text-muted:    light-dark(#3d4550, #d0d6dd);
    --border-subtle: light-dark(#9aa4b2, #6b747f);
  }
}

這是低成本、高回報的一段 CSS:幾行就能讓有需求的使用者拿到明顯更好的可讀性,而其他人完全不受影響。

下一步:Android 差異