
本文首發於政采雲前端博客:深色模式適配指南
背景
隨着 iOS 13 的發布,深色模式(Dark Mode)越來越多地出現在大眾的視野中,支持深色模式已經成為現代移動應用和網站的一個潮流,前段時間更是因為微信的適配再度引起熱議。
深色模式不僅可以大幅減少電量的消耗,減弱強光對比 ,還能提供更好的可視性和沉浸感。
那針對一款 App 應用(原生 + H5)怎么進行深色模式的適配呢?今天就讓我們一起來探究吧!
系統兼容
想要實現深色模式的效果,前提條件是要系統支持, 目前 常見系統支持情況如下:

H5 深色適配
隨着深色模式的流行,越來越多的操作系統、瀏覽器開始支持深色模式,現在可以利用 CSS 的媒體查詢方法(prefers-color-scheme)以及 CSS 變量(CSS variables、CSS custom properties)就可以實現頁面主題跟隨系統自動切換深淺模式 。CSS 變量除了 IE ,其余各大瀏覽器都支持的比較好, 但 prefers-color-scheme 方法還處於 W3C 草案規范,需要對不兼容瀏覽器做向下兼容,具體瀏覽器兼容性可以查詢 Can I Use, 綜合來說,高版本的主流瀏覽器都已經支持,IE 不支持。
可以通過以下兩種方式來實現 Web 端的深色適配:
一、CSS 的媒體查詢
prefers-color-scheme 是一種用於檢測用戶是否有將系統的主題色設置為亮色或者暗色的 CSS 媒體特性。利用其設置不同主題模式下的 CSS 樣式,瀏覽器會自動根據當前系統主題加載對應的 CSS 樣式。light 適配淺色主題,dark 適配深色主題,no-preference 表示獲取不到主題時的適配方案。
- CSS
@media (prefers-color-scheme: light) {
.article {
background:#fff;
color: #000;
}
}
@media (prefers-color-scheme: dark) {
.article {
background:#000;
color: white;
}
}
@media (prefers-color-scheme: no-preference) {
.article {
background:#fff;
color: #000;
}
}
- link 標簽
<link href="./common.css" rel="stylesheet" type="text/css" />
<link href="./light-mode-theme.css" rel="stylesheet" type="text/css" />
<link href="./dark-mode-theme.css" rel="stylesheet" type="text/css" media="(prefers-color-scheme: dark)" />
來看一下效果,將系統設置為淺色外觀:


然后將系統設置為深色外觀:

頁面已經加載了對應深色主題的樣式:

二、CSS 變量 + 媒體查詢
window.matchMedia 方法可以用來查詢指定的媒體查詢字符串解析后的結果。 結合 CSS 變量和 matchMedia 的查詢結果,設置對應的 CSS 主題顏色。該方法更靈活,可以單獨抽離主題色進行適配。
CSS 變量的作用域與 CSS 的"層疊"規則一致,優先級最高的聲明生效。所以當 body 上存在 "dark" 類名時,:root .dark 會生效,否則 :root 生效。
.article {
color: var(--text-color, #eee);
background: var(--text-background, #fff);
}
:root {
--text-color: #000;
--text-background: #fff;
}
:root .dark {
--text-color: #fff;
--text-background: #000;
}
使用 matchMedia 匹配主題媒體,深色模式匹配 (prefers-color-scheme: dark)
,淺色模式匹配 (prefers-color-scheme: light)
。
監聽主題模式,深色模式時為 body 添加類名 dark,根據 CSS 變量的響應式布局特點,自動生效 dark 類名下的 CSS。
const darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
if (darkMode && darkMode.matches) {
document.body.classList.add('dark');
}
darkMode && darkMode.addEventListener('change', e => {
if (e.matches) {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
}
});


那么,針對不支持 CSS 變量的 IE 瀏覽器怎么辦呢?不做兼容性處理的話那頁面可能就是一團糟了。所以我們需要針對不兼容的瀏覽器做一些兜底處理,這里我們可以在 webpack 等構建工具中借助 post-css 的 postcss-css-variables 插件來自動解析 CSS 變量對應的色值,並在原始 CSS 定義之上添加一條新的 CSS 樣式 , 做到對不支持 CSS 變量瀏覽器的兼容 。
用法如下:
module.exports = {
plugins: {
"postcss-css-variables": {
preserve: true,
preserveInjectedVariables: false,
variables: require("./page.json"),
}
}
};


項目實踐
現在的 Web、App 項目大都引用第三方開源組件庫,組件庫一般會使用 Sass、Less 等 CSS 預處理器定義顏色變量作為組件的基礎色值,並單獨抽離為配置文件。所以,項目使用組件庫時可以修改基礎色值來自定義主題。那么針對項目的深色模式適配方案也一樣,主要分為三步:一、組件庫深淺色主題適配 ; 二是、項目中深淺色的顏色適配 ; 三、 完成 CSS 變量到頁面的注入。
組件庫樣式、自定義樣式適配
如果第三方組件本身支持多主題或者深色模式,可以直接按說明給組件設置對應主題模式;如果第三方組件庫不支持的話,只能用覆蓋的方式。這里以 Less 為例進行簡單實例說明:
修改前:
// index.less
@white: #fff; // 顏色預定義
@background-color: @white; // 組件樣式 panel.less
.panel-background-color {
background-color: @background-color; // 組件中使用 less 變量定義顏色樣式
}
新增兩個 js 或者 JSON 文件,分別定義深淺模式下的 CSS 變量, 並命名為 light-theme1.js、dark-theme1.js 他們並不會影響組件的樣式,只是便於后期注入到全局 style 中。
修改后:
// 淺色主題文件 light-theme1.js
const bgColor = '#fff';// 顏色預定義
module.exports = {
"--background-color": bgColor;
}
// 深色主題文件 dark-theme1.js
const bgColor = '#000';// 顏色預定義
module.exports = {
"--background-color": bgColor;
}
// 組件樣式 panel.less
.panel-background-color {
background-color: var(--background-color); //組件中顏色樣式
}
CSS 變量支持第二參數,當變量不存在或者未注冊成功時,可以為其設置默認值,優化如下:
// 組件樣式 panel.less
.panel-background-color {
background-color: var(--background-color, @background-color); // 組件中顏色樣式,其中 @background-color 代表修改前組件的背景顏色變量,這里設其為默認值,在適配不成功情況下,可以保持適配前的樣式。
}
項目才是真正使用組件的地方,並且項目本身也有很多自定義 CSS 的顏色樣式,需要做與組件庫類似的處理,結果也會得到兩個 js / json 文件,分別命名為 light-theme2.js 、dark-theme2.js。
CSS 注入
在頁面渲染前,需要把定義深淺樣式的 CSS 變量注入到頁面。
以上兩步得到了四個文件,合並淺色樣式文件 light-theme1.js 和 light-theme2.js 得到 light-theme.js,合並深色樣式文件dark-theme1.js 和 dark-theme2.js 得到 dark-theme.js, 最后 把 light-theme.js、 dark-theme.js 兩個文件注入到頁面中,注入腳本如下:
import lightTheme from './light-theme';
import darkTheme from './dark-theme';
const createStyle = (content) => {
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = content;
document.getElementsByTagName("script")[0].parentNode.appendChild(style);
const createCssStyle = () => {
const lightThemeStr = Object.keys(lightTheme).map(key => key + ':' + lightTheme[key]).join(';');
const darkThemeStr = Object.keys(darkTheme).map(key => key + ':' + darkTheme[key]).join(';');
const lightContent = `body{${lightThemeStr}}`;
const darkContent = `body.dark{${darkThemeStr}}`;
createStyle(lightContent);
createStyle(darkContent);
isDarkSchemePreference();
};
注入完成后,項目頁面中就有了 css 變量定義,包括淺色模式 CSS 變量定義和深色模式 CSS 變量定義,具體哪一個生效,就可以根據上面提到的兩種適配方案給 body 添加 class 來控制 。 默認時淺色模式生效,添加 dark
類名時,深色模式會生效。至此就實現了一套完整的深色模式適配方案。
native 深色適配
iOS
在 iOS 系統中, 開發者從顏色和圖片兩個方面來進行適配,我們不需要關心切換模式后該怎么操作,因為這些都由系統幫我們實現。顏色的適配,需要使用系統提供的 API,在回調用中不同的模式下分別設置顏色,而圖片的適配,需要在 XCode 的工具欄中 Appearances 下選擇 Any,Dark,在同一名稱資源的配置下分別添加圖片資源。當切換深色模式時,系統會根據適配的顏色和圖片資源進行查找和自動切換對應模式下的顏色和資源文件。
Android
安卓在 Android 10(API 級別 29)及更高版本中提供深色主題背景,可以通過以下三種方法啟用深色主題背景:
- 使用系統設置(Settings -> Display -> Theme)啟用深色主題背景
- 使用"快捷設置"圖塊,從通知托盤中切換主題背景(啟用后)
- 在 Pixel 設備上,選擇"省電模式"將同時啟用深色主題背景,其他原始設備制造商 (OEM) 不一定支持這種行為
在應用中支持深色主題背景
如要支持深色主題背景,必須將應用的主題背景(通常可在 res/values/styles.xml
中找到)設置為繼承 DayNight
主題背景:
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
還可以使用 MaterialComponent 的深色主題背景:
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
這會將應用的主要主題背景與系統控制的夜間模式標記相關聯,並將應用的默認主題背景設置為深色主題背景(如果已啟用)。
主題背景和樣式
主題背景和樣式應避免使用,旨在於淺色主題背景下使用的硬編碼顏色或圖標,您應改用主題背景屬性(首選)或適合在夜間使用的資源,以下是需要了解的兩個最重要的主題背景屬性:
-
?android:attr/textColorPrimary
這是一種通用型文本顏色,它在淺色主題背景下接近於黑色,在深色主題背景下接近於白色,該顏色包含一個停用狀態。 -
?attr/colorControlNormal
一種通用圖標顏色,該顏色包含一個停用狀態。
Flutter
這里以 Flutter 為例,簡單介紹下跨平台開發框架如何適配深色模式。Flutter 定義主題有兩種方式:全局主題或使用 Theme 來定義應用程序局部的顏色和字體樣式。
全局主題
全局主題就是有應用程序根 MaterialAPP 創建 的 Theme。為了在整個應用程序中共享包含顏色和字體樣式的主題,我們可以提供 ThemeData 給 Material 的構造函數。theme 指定的是淺色模式,darkTheme 指定的是深色模式,程序會根據系統設定的暗黑模式自動匹配模式。
new MaterialApp(
title: title,
theme: new ThemeData(
brightness: Brightness.light,
primaryColor: Colors.lightBlue[800],
accentColor: Colors.cyan[600] ,
),
darkTheme: new ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.lightGreen[800] ,
accentColor: Colors.cyan[200],
),
);
局部主題
如果我們想在應用程序的一部分中覆蓋應用程序的全局的主題,我們可以將要覆蓋得部分封裝在一個 Theme 的 Widget 中,有 2 種方法可解決:創建特有的 ThemeData 或擴展父主題。
創建特有的 ThemeData
如果我們不想繼承任何應用程序的顏色或字體樣式,我們可以通過 new ThemeData()
創建一個實例並將其傳遞給 Theme Widget。
new Theme(
data: new ThemeData(
accentColor: Colors.yellow,
),
child: new FloatingActionButton(
onPressed: () {},
child: new Icon(Icons.add),
),
);
擴展父主題
擴展父主題時無需覆蓋所有的主題屬性,我們可以通過使用 copyWith
方法來實現。
new Theme(
data: Theme.of(context).copyWith(accentColor: Colors.yellow),
child: new FloatingActionButton(
onPressed: null,
child: new Icon(Icons.add),
),
);
使用主題
我們可以在 Widget 的 build
方法中通過 Theme.of(context)
函數使用自定義的主題。
new Container(
color: Theme.of(context).accentColor,
child: new Text(
'Text with a background color',
style: Theme.of(context).textTheme.title,
),
);
渲染效果 如下 :


總結
以上分別介紹了在 App 應用中對 H5 頁面和客戶端的深色模式適配方案,當然其中 H5 的方案頁同樣適應於 PC 端。使用前一定要確保你的系統和瀏覽器是兼容深色模式的,不然就沒有效果了呢。本篇只簡單介紹了幾種方案,歡迎有更好想法的小伙伴一起討論~
參考資料
- developer.mozilla.org/en-US/docs/…
- juejin.im/post/5eca7c…
- developer.android.com/guide/topic…
- flutterchina.club/cookbook/de…