prefers-color-scheme 與 color-scheme 名字相近,作用方向完全相反,混淆這兩者是雙主題實作最常見的根本錯誤。前者是媒體查詢,方向是「瀏覽器告訴你使用者想要什麼」;後者是CSS 屬性,方向是「你告訴瀏覽器這份文件支援什麼」。只寫前者的網站會出現白色捲軸配深色頁面、下拉選單是亮的、輸入框是白底的這類「一半沒換」的畫面——因為瀏覽器從沒被告知它可以用暗色去畫那些原生元件。
prefers-color-scheme:讀取偏好
值只有 light 與 dark 兩個。規範早期的 no-preference 已被移除,現在「沒有明確偏好」會回報為 light。這代表你無法用 CSS 區分「使用者選了亮色」與「使用者沒選」——如果你的產品需要這個區別,只能靠自己的儲存狀態,見 三態切換器與偏好儲存。
/* 亮色為基準,暗色覆寫 */
.card { background: #fff; color: #1a1a1a; }
@media (prefers-color-scheme: dark) {
.card { background: #1c1c1e; color: #f2f2f7; }
}Baseline 狀態:widely available,2020 年 1 月起跨瀏覽器可用。實務上不需要 fallback,只需要注意在完全不支援的環境下媒體查詢不成立,因此把亮色寫在查詢外面當預設是比較安全的寫法(反過來把暗色寫成預設,會讓不支援的環境拿到暗色而使用者沒得選)。
JS 端對應的讀法與監聽:
const mq = window.matchMedia('(prefers-color-scheme: dark)');
mq.matches; // 目前是否偏好暗色
mq.addEventListener('change', (e) => { // 使用者在系統切換時即時反應
applyTheme(e.matches ? 'dark' : 'light');
});addEventListener 是現在的正確寫法,舊教學裡的 mq.addListener 已廢棄。忘記監聽 change 是「使用者在系統設定裡切換,但頁面沒跟著變」這個 bug 的直接原因,尤其在 iOS 自動日夜切換與 Android 的排程暗色下很容易被抓到。
color-scheme:宣告能力
color-scheme 決定 UA 用哪一套預設去畫你沒有畫的東西。MDN 列出它影響四類:畫布(canvas surface)底色、捲軸與互動 UI 的預設色、表單控件的預設色、以及瀏覽器提供的 UI(例如拼字檢查底線)。
:root {
color-scheme: light dark; /* 兩套都支援,順序代表偏好 */
}可用值與語意:
| 值 | 意義 |
|---|---|
normal | 不表態,使用頁面預設 |
light | 只用亮色配置 |
dark | 只用暗色配置 |
light dark | 兩者皆可,亮色優先 |
dark light | 兩者皆可,暗色優先 |
only light | 禁止 UA 覆寫(可關掉 Chrome Auto Dark Theme) |
color-scheme 是可繼承屬性,所以可以做局部覆寫:
header { color-scheme: only light; }
main { color-scheme: light dark; }
footer { color-scheme: only dark; }Baseline 狀態:widely available,2022 年 1 月起。
meta 版本:為什麼一定要有
同樣的宣告有 HTML 版本:
<meta name="color-scheme" content="light dark">MDN 明確建議把它放在任何 CSS 樣式資訊之前,理由是避免載入期間的畫面閃爍。差別在時機:CSS 裡的 :root { color-scheme } 要等樣式表下載並套用才生效,而 meta 在 HTML parser 讀到的那一刻就生效。對外部樣式表阻塞的頁面來說,這中間有一段時間畫布會是預設白色——這就是很多人以為自己已經處理好、實際上還是會白閃一下的原因。
content 的值與 CSS 相同,但 only dark 是無效值:規範不允許在文件不支援亮色時強制暗色。
兩者一起用才是完整的一組
正確的最小組合是三件事同時到位:
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="color-scheme" content="light dark"> <!-- ① 早期宣告 -->
<link rel="stylesheet" href="/style.css">
</head>:root {
color-scheme: light dark; /* ② CSS 宣告,供後續 light-dark() 使用 */
--surface: light-dark(#ffffff, #14161a);
--text: light-dark(#1a1d21, #e6e8eb);
}
body { background: var(--surface); color: var(--text); } /* ③ 自己畫的部分 */少了 ①②,原生控件與捲軸不會換色;少了 ③,只有 UA 元件換色而你的版面沒換。這兩種「只換一半」的畫面,是雙主題最容易被使用者發現的破綻。
一個常被忽略的行為:canvas 底色
當 color-scheme 生效且系統偏好暗色時,UA 會把畫布底色改成深色。這代表即使你完全沒寫 body { background },頁面也會是深色的。反過來說,如果你在 body 上寫死了 background: #fff,而 html 沒寫,那麼 iOS Safari 的橡皮筋回彈區域、以及超出 body 高度的區域,會露出 UA 的深色畫布——出現「內容白、上下黑」的夾心畫面。解法是把主背景色也套到 html 上,細節見 Mobile web 的差異。
與 iframe、SVG 的關係
嵌入的 <iframe> 與 SVG 內部,prefers-color-scheme 會依父層的 color-scheme 求值。這在兩個場景會咬人:一是第三方嵌入(YouTube、地圖、金流表單)跟著你的宣告走,但它們自己的實作未必支援;二是外連的 .svg 檔案有自己的樣式脈絡,用 currentColor 會比在 SVG 內寫媒體查詢可靠得多,見 圖片、圖示與 media。