「怎麼用 JS 偵測頁面有沒有被強制反色」是一個問錯的問題,但值得完整回答,因為理解為什麼偵測不可靠,才能理解正確解法為什麼是宣告而不是偵測。
為什麼 JS 偵測不可靠
UA 的演算法反色發生在繪製階段,不在樣式計算階段。getComputedStyle(el).backgroundColor 回傳的是樣式計算的結果,也就是你宣告的值,不是螢幕上真正的像素顏色。Android WebView 的 algorithmic darkening 與 Chrome 的 Auto Dark Theme 都是這個模式。
同時,DOM 沒有任何 API 能讀回自己被繪製後的像素——html2canvas 之類的函式庫是重新渲染一次,不是截圖,所以它讀到的也是宣告值。Canvas 的 getImageData 只能讀你自己畫進 canvas 的內容,讀不到 DOM 的合成結果。
因此,沒有可靠、標準的 JS 方法能判斷頁面是否被 UA 反色。 網路上流傳的幾種嘗試都有明確的失敗模式:
| 嘗試 | 為什麼不可靠 |
|---|---|
讀 getComputedStyle 比對預期值 | 讀到的是宣告值,反色不影響它 |
用 canvas 畫已知色再 getImageData | canvas 內容不受 DOM 反色影響,永遠讀到原色 |
檢查 matchMedia('(prefers-color-scheme: dark)') | UA 反色路徑下這個值是 false,正好相反 |
| UA 字串嗅探 | 版本一改就失效,且無法得知使用者的 Chrome 設定 |
比對 Canvas/CanvasText 系統色 | 這些在 forced-colors 才有意義,反色模式下不變 |
有一個間接訊號在實務上偶爾有用:matchMedia('(prefers-color-scheme: dark)') 為 false,但你知道自己的頁面在暗色環境常被回報異常——這只是統計上的懷疑,不是判定。
正確解法:讓觸發條件不成立
Android 官方文件把 algorithmic darkening 的觸發條件寫得很清楚,第一條就是「網頁內容沒有使用 prefers-color-scheme」。Chrome 的 Auto Dark Theme 也是針對「亮色主題網站」。所以正解是:
支援雙主題 → 條件不成立 → 反色不會發生。 不需要偵測,也不需要對抗。
最小組合(重複一次,因為這是本篇的結論):
<meta name="color-scheme" content="light dark">:root {
color-scheme: light dark;
--bg: light-dark(#ffffff, #0e1013);
--fg: light-dark(#16191d, #e6e8eb);
}
html, body { background: var(--bg); color: var(--fg); }只做亮色的頁面則用 only light 明確退出:
<meta name="color-scheme" content="only light">:root { color-scheme: only light; }only 這個關鍵字的存在目的就是禁止 UA 覆寫,Chrome 官方文件把它列為 Auto Dark Theme 的退出方式;Android WebView 的darkening 也遵守作者的明確關閉。
局部退出:顏色即資料的區塊
即使整頁支援雙主題,仍有些區塊的顏色不能被任何機制改動。color-scheme 可繼承,所以可以只圈住那個子樹:
.color-swatches, /* 色票 */
.print-preview, /* 列印預覽 */
.map-legend, /* 地圖圖例 */
.medical-image { /* 醫療影像 */
color-scheme: only light;
forced-color-adjust: none; /* 連 Windows 高對比模式也不要動 */
}forced-color-adjust: none 要謹慎——MDN 明確說它應該用於支援使用者的顏色與對比需求,不是保住品牌色。上面這幾類(顏色本身承載資訊)是它少數的正當用途。
完整測試矩陣
雙主題的驗證不能只在桌機 DevTools 切一下。以下是可以逐格打勾的矩陣,優先級由上而下:
| # | 環境 | 設定 | 預期結果 |
|---|---|---|---|
| 1 | 桌面 Chrome | 系統亮 | 亮色,捲軸/表單為亮 |
| 2 | 桌面 Chrome | 系統暗 | 暗色,捲軸/表單為暗 |
| 3 | 桌面 Chrome | 系統暗 + 手動切亮 | 全亮,含原生控件 |
| 4 | 桌面 Chrome | 系統亮 + 手動切暗 | 全暗,含原生控件 |
| 5 | 桌面 Chrome | Slow 4G + 硬重整(四種組合) | 第一幀無白閃 |
| 6 | 桌面 Chrome | 停用 JavaScript | 跟隨系統偏好正確渲染 |
| 7 | 桌面 Chrome | Rendering → Emulate auto dark mode | 網站已支援暗色,不應被額外反色 |
| 8 | 桌面 Chrome/Edge | Rendering → forced-colors: active | 版面可用、焦點環可見、狀態可辨 |
| 9 | Safari macOS | 系統暗 | 暗色 + theme-color 染色工具列 |
| 10 | iOS Safari | 系統暗 | 暗色;捲到頂/底無白邊 |
| 11 | iOS Safari | 加到主畫面(standalone) | 狀態列樣式合理 |
| 12 | Android Chrome | 系統暗 + Auto Dark Theme 開啟 | 顯示你的暗色主題,非演算法版本 |
| 13 | Android Chrome | 協助工具字級 200% | 版面不破 |
| 14 | Facebook 內建瀏覽器 | app 淺色 / 深色各一次 | 兩種都正確;記錄診斷頁輸出 |
| 15 | Instagram 內建瀏覽器 | 同上 | 同上 |
| 16 | LINE 內建瀏覽器 | 同上 | 同上,並確認 localStorage 是否可用 |
| 17 | 自家 app WebView | 若有 | 與設計一致 |
第 5、6、7、8 這四格是最容易被跳過、也最容易藏 bug 的。第 6 格(停用 JS)的預期結果值得再強調:應該跟隨系統偏好正確渲染。如果停用 JS 後只剩亮色,代表系統偏好的判斷被錯誤地放進了 JS。
桌面模擬工具速查
- Chrome DevTools:
Cmd/Ctrl+Shift+P→Show Rendering→Emulate CSS media feature prefers-color-scheme、Emulate auto dark mode、Emulate CSS media feature forced-colors、Emulate CSS media feature prefers-contrast。 - Safari Web Inspector:Elements 面板工具列有亮/暗切換鈕。
- Firefox DevTools:Inspector 右上角有亮/暗與
prefers-contrast的切換。 - Android 實機的 Auto Dark Theme:
chrome://flags啟用#darken-websites-checkbox-in-theme-setting,再到設定 → 主題勾選。
模擬永遠只是近似。Auto Dark Theme 與 in-app WebView 的最終驗證必須在實機上做,因為模擬器的演算法版本、app 的注入行為都無法完整重現。
把驗證自動化
視覺回歸測試可以擋住大部分退化。Playwright 支援直接指定色彩配置:
// playwright.config.js 用兩個 project 跑同一組測試
projects: [
{ name: 'light', use: { colorScheme: 'light' } },
{ name: 'dark', use: { colorScheme: 'dark' } },
]test('首頁視覺', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot(); // 兩個 project 各產生一張基準圖
});對比檢查可以用 axe 之類的工具在 CI 跑,同樣要跑兩次:
const { AxeBuilder } = require('@axe-core/playwright');
test('對比檢查', async ({ page }) => {
await page.goto('/');
const r = await new AxeBuilder({ page }).withTags(['wcag2aa']).analyze();
expect(r.violations).toEqual([]);
});兩套主題各跑一次是這裡的重點。只跑一次的 CI 會給人「已經測過無障礙」的錯覺,而暗色主題的對比問題完全不會被抓到。
至於 Auto Dark Theme 與 in-app WebView,目前沒有成熟的自動化方案,只能靠上一篇的診斷頁做人工檢查表。誠實記錄「這一格是人工測的、上次測是什麼時候」,比假裝它被自動化涵蓋要有用得多。
下一步:常見錯誤與排錯手冊。