介面顏色換完之後,剩下的破綻幾乎都在圖片上:白底 PNG logo 在深色頁面變成一塊刺眼的亮斑、截圖裡的白色瀏覽器介面在暗色文章中格外突兀、SVG 圖示因為填了寫死的 #333 而消失。這些問題各有不同的正確解法,不能全部靠一個 filter: invert() 蓋過去。
決策樹:這張圖該用哪種做法
| 圖的類型 | 正確做法 |
|---|---|
| 圖示(單色、功能性) | inline SVG + currentColor |
| Logo(多色、有品牌規範) | <picture> 提供兩個檔案 |
| 插畫(自製、可控) | inline SVG 內用 CSS 變數,或提供兩版 |
| 照片、螢幕截圖 | 保留原圖,加保護底色與細微降低亮度 |
| 圖表、資料視覺化 | 用 token 重繪,不要用 filter |
| 使用者上傳內容 | 一律不動,只加底色 |
圖示:currentColor 是唯一正確答案
單色圖示不該有自己的顏色,它應該繼承文字色:
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path d="M4 10h12M10 4v12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>只要外層文字色是 token 化的,圖示自動在兩套主題正確,也自動在 hover、disabled、錯誤狀態下正確——這是 filter: invert() 永遠做不到的,因為 filter 只知道反轉,不知道語意。
currentColor 只在 inline SVG 或用 <use> 引入同源 SVG 時生效。用 <img src="icon.svg"> 引入的 SVG 是獨立文件,繼承不到外層的 color,這是最常見的誤解。若因為快取或架構因素必須用 <img>,退而求其次的做法是 CSS mask:
.icon {
background-color: currentColor; /* 顏色由 CSS 控制 */
mask: url("/icons/plus.svg") center / contain no-repeat;
width: 20px; height: 20px;
}這個做法把 SVG 當形狀、顏色交給 CSS,效果等同 currentColor,且可以用 <img> 一樣的快取策略。
Logo:用 <picture> 提供兩個檔案
品牌 logo 通常有多色與明確規範,反轉會違反規範。正解是準備兩個檔案,讓瀏覽器依偏好挑:
<picture>
<source srcset="/logo-dark.svg" media="(prefers-color-scheme: dark)">
<img src="/logo-light.svg" alt="站名" width="120" height="32">
</picture>但這個寫法有一個必須知道的限制:<picture> 只認得系統偏好,不認得你的手動覆寫。 使用者在系統亮色下手動切成暗色主題時,logo 仍會是亮色版。
若產品有手動切換,要用 CSS 控制可見性而非 <picture>:
<img class="logo logo--light" src="/logo-light.svg" alt="站名">
<img class="logo logo--dark" src="/logo-dark.svg" alt="" aria-hidden="true">.logo--dark { display: none; }
:root[data-theme="dark"] .logo--light { display: none; }
:root[data-theme="dark"] .logo--dark { display: block; }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .logo--light { display: none; }
:root:not([data-theme="light"]) .logo--dark { display: block; }
}代價是兩個檔案都會被下載。對一個 3KB 的 SVG logo 這通常可接受;對大圖則應該用 inline SVG + CSS 變數的做法避免雙下載。
支援 light-dark() 影像值的環境可以更簡潔,但要用 @supports 保護,因為影像值的支援度落後於顏色值:
@supports (background-image: light-dark(url("a.svg"), url("b.svg"))) {
.logo {
background-image: light-dark(url("/logo-light.svg"), url("/logo-dark.svg"));
}
}插畫:inline SVG + CSS 變數
自製插畫最理想的做法是把它 inline 進 HTML,內部用變數:
<svg viewBox="0 0 200 120" role="img" aria-label="流程示意">
<style>
.bg { fill: var(--illus-bg, #f2f4f7); }
.line { stroke: var(--illus-line, #98a2b3); }
.key { fill: var(--illus-key, #3b6fd4); }
</style>
<rect class="bg" width="200" height="120" rx="8"/>
<path class="line" d="M20 60h160" stroke-width="2"/>
</svg>:root {
--illus-bg: light-dark(#f2f4f7, #1c1f24);
--illus-line: light-dark(#98a2b3, #5b6470);
--illus-key: light-dark(#3b6fd4, #8fb3f0);
}外連的 .svg 檔案沒辦法讀到父文件的自訂屬性(它是獨立的樣式脈絡),但它可以自己寫媒體查詢:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 120">
<style>
:root { color-scheme: light dark; }
.bg { fill: light-dark(#f2f4f7, #1c1f24); }
</style>
...
</svg>這個做法在 <img> 引入時可運作(SVG 內的媒體查詢依父層 color-scheme 求值),但同樣只跟隨系統偏好、不跟隨手動覆寫。
照片與螢幕截圖:不要反轉
照片絕對不能反轉——人臉會變成負片。web.dev 提到的研究指出使用者在暗色模式下偏好較不鮮豔的影像,因此可行的做法是細微降低亮度或飽和度,而不是改變它:
@media (prefers-color-scheme: dark) {
img:not([src$=".svg"]):not(.no-dim) {
filter: brightness(0.88) saturate(0.9);
}
}這個處理要非常克制。降太多會讓內容看起來像壞掉、也會破壞需要真實呈現的內容(商品圖、醫療影像、設計作品)。給一個 .no-dim 的逃生門是必要的。
螢幕截圖是最麻煩的一類,因為截圖裡通常有大面積白色介面。三個實際做法,依成本排序:
- 加圓角邊框與內縮,讓白色區塊看起來像刻意的「畫框」而非破圖。成本最低,效果意外地好。
- 重新截一份暗色版,用
<picture>或 class 切換。成本高但效果最好,適合文件站與產品官網。 - 改用可縮放的向量重繪(用 SVG 或直接用 HTML/CSS 重現介面),一勞永逸且可被搜尋、可被翻譯。
無論哪種,都應該給圖片一個保護底色,避免透明 PNG 在深色背景上出現不可預期的結果:
img { background-color: light-dark(#ffffff, #1c1f24); }使用者上傳內容:一律不動
留言區的圖、頭像、商品照片,這些內容你無法預測,任何 filter 都可能產生災難。正確做法是不處理,只確保容器有合理底色與邊框,讓它在深色頁面上看起來是「一張圖」而不是「一塊破洞」。
favicon 與 OG 圖
favicon 也有雙主題版本,做法是在 SVG favicon 內寫媒體查詢:
<!-- favicon.svg -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<style>
path { fill: #16191d; }
@media (prefers-color-scheme: dark) { path { fill: #e6e8eb; } }
</style>
<path d="..."/>
</svg><link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="icon" href="/favicon.ico" sizes="32x32"> <!-- 舊瀏覽器 fallback -->支援度依瀏覽器而異,且分頁列的背景色未必等於系統偏好,所以不要依賴它——比較保險的是選一個在深淺底上都能看清楚的 favicon 設計(有描邊或有底色),這比做兩版更耐用。
OG 分享圖(og:image)沒有雙主題機制,社群平台會用自己的背景顯示它。做法是永遠給 OG 圖一個不透明底色,不要用透明背景,否則在不同平台的深淺介面上會有不同結果。
影片與 Canvas
<video> 內容不做處理,但海報圖(poster)與播放器控制列要跟著主題走。Canvas 與 WebGL 完全不吃 CSS,主題切換時必須主動重繪:
function onThemeChange() {
const cs = getComputedStyle(document.documentElement);
chart.setColors({
axis: cs.getPropertyValue('--text-muted').trim(),
grid: cs.getPropertyValue('--border-subtle').trim(),
});
chart.render();
}從 CSS 自訂屬性讀顏色而不是在 JS 裡另寫一份色表,是避免兩份顏色來源不同步的關鍵。這一點在圖表庫的整合上特別容易出錯。