以症狀為入口的排錯手冊。每一條給出觀察到的現象 → 機制上的原因 → 具體修法

載入時白閃一下

現象:重新整理時看到一瞬間的白色,然後才變暗。網路愈慢愈明顯。

原因:先分辨是哪一種。開 DevTools 節流到 Slow 4G,錄一段 Performance 逐幀看:若白閃出現在任何 CSS 套用之前,是宣告太晚;若白閃出現在樣式已套用之後、JS 執行之前,是手動覆寫的還原太慢。

修法:前者加 <meta name="color-scheme" content="light dark"><head> 最前面(CSS 之前)。後者加首屏 inline script,且不可 deferasynctype=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() 一律回傳第一個參數。

修法:在 :rootcolor-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)。

排錯的通用順序

遇到說不清楚的主題問題時,照這個順序刪除變因,通常三步內就能定位:

  1. <html> 的實際狀態document.documentElement.dataset.themegetComputedStyle(document.documentElement).colorScheme。這兩個值就是全部的狀態。
  2. 看媒體查詢的答案matchMedia('(prefers-color-scheme: dark)').matches。若它與畫面不一致,問題在 UA 反色或環境(WebView),不在你的 CSS。
  3. 看某個具體元素的計算值getComputedStyle(el).backgroundColor。若計算值是對的但畫面不對,就是繪製階段被動了手腳——回到第 2 步的結論。

下一步:主張 vs 可佐證,與延伸資源