以症狀為入口的排錯手冊。每一條給出觀察到的現象 → 機制上的原因 → 具體修法。
載入時白閃一下
現象:重新整理時看到一瞬間的白色,然後才變暗。網路愈慢愈明顯。
原因:先分辨是哪一種。開 DevTools 節流到 Slow 4G,錄一段 Performance 逐幀看:若白閃出現在任何 CSS 套用之前,是宣告太晚;若白閃出現在樣式已套用之後、JS 執行之前,是手動覆寫的還原太慢。
修法:前者加 <meta name="color-scheme" content="light dark"> 到 <head> 最前面(CSS 之前)。後者加首屏 inline script,且不可 defer/async/type=module,並確認它沒有被打包工具搬到 bundle 裡。完整版見 避免主題閃爍。
頁面是暗的,但輸入框、下拉、捲軸還是亮的
原因:只改了 CSS 自訂屬性,沒有設 color-scheme。UA 繪製的元件不看你的變數。
修法:
:root { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }切換器的 JS 也要一起改(root.style.colorScheme = value),不要只改 data-theme 而依賴 CSS——在 CSS 尚未載入的瞬間仍會不同步。
手動切成亮色,但系統是暗色時切不動
原因:@media (prefers-color-scheme: dark) 區塊與 :root[data-theme="light"] 選擇器權重相同,結果由宣告順序決定,而媒體查詢通常寫在後面。
修法:讓兩者互斥,不要依賴順序:
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { /* 暗色值 */ }
}在系統設定切換日夜,頁面沒跟著變
原因:兩種可能。一是把系統偏好的判斷寫進了 JS 並寫死成 data-theme,之後媒體查詢就再也不參與;二是有監聽但用了已廢棄的 mq.addListener。
修法:系統偏好交給 CSS 媒體查詢處理,JS 只負責手動覆寫。需要 JS 反應時用 mq.addEventListener('change', …),並且只在「跟隨系統」狀態下才動作。
兩個分頁的主題不同步
原因:沒有監聽 storage 事件。
修法:見 三態切換器 的實作。注意 storage 只在其他分頁觸發,不會造成回圈。
頁面上下露出白邊(iOS 明顯)
原因:背景色只設在 body,過捲的橡皮筋區域與超出 body 的區域露出 html 或 UA 畫布的顏色。
修法:html { background-color: var(--bg-canvas); }。若用了 viewport-fit=cover,同時檢查安全區域的 padding。
頁面被反成暗色,但我的暗色 CSS 沒生效
原因:UA 演算法反色正在作用。Android WebView 走 UA 反色路徑時,@media (prefers-color-scheme: dark) 會求值為 false,所以你的暗色樣式當然不會套用,畫面的暗是 UA 疊上去的。
修法:確保頁面確實使用了 prefers-color-scheme 並有 <meta name="color-scheme"> 宣告——只要條件成立,UA 就會讓開。診斷方式見 強制反色的偵測與對抗 的診斷頁:prefers-color-scheme: dark 為 false 但畫面是暗的,就是這一種。
同一個網址在不同 app 裡主題不一樣
原因:In-app WebView 的 prefers-color-scheme 來自宿主 app 的佈景(Android 的 isLightTheme、iOS 的 overrideUserInterfaceStyle),不是使用者的系統設定。
修法:這不是能修的東西,是必須接受的環境變數。做法是兩套主題都要完整可用,並在 in-app 情境不要假設使用者能切換或偏好會保存。細節見 In-app WebView 專篇。
使用者的主題選擇在 app 內建瀏覽器裡不會保存
原因:部分 in-app browser 使用非持久化的儲存,或每次開啟即新 session。
修法:接受降級(回到系統/app 預設),並確保 localStorage 讀寫都包在 try/catch 裡——Safari 無痕模式與封鎖儲存的環境會直接丟例外,未捕捉會中止整段首屏腳本,造成比「沒保存」嚴重得多的問題。
暗色下圖片刺眼
原因:白底 PNG/JPG 直接放在深色背景上,亮度差過大。
修法:依圖片類型分流,不要一律 filter。圖示改 inline SVG + currentColor;logo 提供兩版;照片可細微降亮度/飽和度並保留逃生門;截圖加邊框內縮或重新截暗色版。完整決策樹見 圖片、圖示與 media。
暗色下卡片全部塌成一片
原因:層次是靠陰影表達的,而陰影在深色背景上幾乎不可見。
修法:暗色改用背景明度表達高程(--surface-0 到 --surface-3 逐階變亮),陰影改為更黑更擴散、只負責分離而非高度。見 對比、暗色不是純黑、高程與陰影。
hover 效果在暗色下消失
原因:hover 底色寫成 rgba(0,0,0,.05),黑疊黑等於沒疊。
修法:把疊層顏色 token 化:--overlay-hover: light-dark(rgb(0 0 0 / .05), rgb(255 255 255 / .07))。
按鈕在暗色下看不清楚
原因:主色從色階的深端搬到暗色主題,但疊在上面的文字色沒有跟著反轉,變成「淺底白字」。
修法:--accent 與 --accent-text 成對定義並成對反轉:
--accent: light-dark(#3b6fd4, #8fb3f0);
--accent-text: light-dark(#ffffff, #0e1013);對比檢查在亮色通過、暗色不過
原因:把亮色的顏色值等比換算到暗色,而不是各自量測。
修法:兩套主題各跑一次 axe / Lighthouse。門檻:一般文字 4.5:1、大字(≥24px 或 ≥18.5px 粗體)3:1、介面元件與必要圖形 3:1。
light-dark() 沒作用,永遠拿到亮色
原因:沒有設 color-scheme。沒有它時 light-dark() 一律回傳第一個參數。
修法:在 :root 加 color-scheme: light dark。若確認有設但仍不對,檢查該元素所在的子樹是不是被覆寫成 only light——light-dark() 依使用該值的元素的 color-scheme 求值,不是宣告它的元素。
圖表/Canvas 不跟著換色
原因:Canvas 與 WebGL 不吃 CSS。
修法:切換主題時主動重繪,並從 CSS 自訂屬性讀顏色(getComputedStyle(root).getPropertyValue('--text-muted')),避免 JS 裡另存一份會不同步的色表。
第三方 iframe 不換色
原因:嵌入內容有自己的實作,多數不支援跟隨父層。
修法:檢查該服務有沒有 theme 參數(YouTube、Giscus、Stripe Elements、reCAPTCHA 多半有),切換主題時重新載入 iframe 或呼叫它的 API。無法控制時,給 iframe 一個中性的容器底色與內距,讓它看起來像刻意的嵌入區塊。
Windows 高對比模式下版面壞掉
原因:用背景色表達的狀態被強制色抹平;background-image 畫的圖示消失;appearance: none 重畫的控件變成看不見的方塊。
修法:@media (forced-colors: active) 區塊裡改用邊框與系統色關鍵字表達狀態,保留焦點環。見 原生控件、捲軸與 forced-colors。
切換時整頁卡頓
原因:對整個文件樹套用 transition: all 或大範圍的顏色 transition,造成一次巨量重繪。
修法:限縮到少數屬性與短時長,包在 @media (prefers-reduced-motion: no-preference) 裡;或改用 View Transitions 做一次性交叉淡入。載入期間應完全禁用 transition(載入完成後再移除 no-transition class)。
排錯的通用順序
遇到說不清楚的主題問題時,照這個順序刪除變因,通常三步內就能定位:
- 看
<html>的實際狀態:document.documentElement.dataset.theme與getComputedStyle(document.documentElement).colorScheme。這兩個值就是全部的狀態。 - 看媒體查詢的答案:
matchMedia('(prefers-color-scheme: dark)').matches。若它與畫面不一致,問題在 UA 反色或環境(WebView),不在你的 CSS。 - 看某個具體元素的計算值:
getComputedStyle(el).backgroundColor。若計算值是對的但畫面不對,就是繪製階段被動了手腳——回到第 2 步的結論。
下一步:主張 vs 可佐證,與延伸資源。