前言
随着苹果发布 iOS 13 正式版,推出了备受期待的深色模式(Dark Mode)。因该特性在浏览时提供更好的可视性和沉浸感。支持深色模式已然成为现代移动应用和网站的一个潮流趋势。
那么就跟随趋势,开始进行深色模式的适配吧。
技术方案
CSS 媒体查询
prefers-color-scheme 是一种用于检测用户是否有将系统的主题色设置为亮色或者暗色的 CSS 媒体特性。利用其设置不同主题模式下的 CSS 样式,浏览器会自动根据当前系统主题加载对应的 CSS 样式。light 适配浅色主题,dark 适配深色主题,no-preference 表示获取不到主题时的适配方案。
@media (prefers-color-scheme: light) {
.content {
color: #333;
background-color: #fff;
}
}
@media (prefers-color-scheme: dark) {
.content {
color: #fff;
background-color: #292934;
}
}
先来看一下效果,将系统设置为浅色外观:
然后将系统设置为深色外观:
CSS 变量
在项目中,往往需要写大量的 CSS 样式类名,如果把样式根据不同的外观模式各写一份,其工作量可想而知。而通过将不同外观模式下的颜色定义为 CSS 变量。在外观模式切换时,只需修改颜色变量即可。
// 示例代码
:root {
--white: #fff;
--gray: #333;
--text-color: var(--gray);
}
@media (prefers-color-scheme: light) {
--text-color: var(--gray);
}
@media (prefers-color-scheme: dark) {
--text-color: var(--white);
}
.container {
color: var(--text-color);
}
Window.matchMedia
浏览器提供了 window.matchMedia 方法,可以用来查询指定的媒体查询字符串解析后的结果。
if (window.matchMedia('prefers-color-scheme: dark').matches) {
// 深色模式做什么
} else {
// 浅色模式做什么
}
另外还可以监听系统外观模式的状态:
window.matchMedia('(prefers-color-scheme: dark)').addListener(e => {
if (e.matches) {
// 系统开启深色模式后做什么
} else {
// 系统开启浅色模式后做什么
}
});
项目实践
以 vue-cli 搭建的应用为例。
组件库定制主题
项目中大都引用第三方开源组件库,组件库一般会使用 Sass、Less 等 CSS 预处理器定义颜色变量作为组件的基础色值,可以修改基础色值来自定义主题和深色模式适配。
例如使用 vant 组件库,代码如下:
:root {
--white: #fff;
--gray-1: #4E4C56;
--gray-2: #f8f8f8;
--dark-1: #292934;
--dark-2: #23232B;
}
@media (prefers-color-scheme: light) {
--text-color: var(--gray-1);
--background-color: var(--gray-2);
--card-background-color: var(--white);
}
@media (prefers-color-scheme: dark) {
--text-color: var(--white);
--background-color: var(--dark-2);
--card-background-color: var(--dark-1);
}
// 覆盖 vant less 样式变量
@text-color: var(--text-color);
@tabbar-background-color: var(--card-background-color);
更多关于 vant 定制主题的内容,可查阅官方文档。
图片显示
如果一张图是暗色调,在明亮模式色彩对比度强、观看流畅,但在暗黑模式下便会存在和背景色对比度弱,不方便查看。所以需要在不同模式下显示不同的图片。实现方式就是使用 picture 元素。
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/25423296/163456776-7f95b81a-f1ed-45f7-b7ab-8fa810d529fa.png" />
<img src="https://user-images.githubusercontent.com/25423296/163456779-a8556205-d0a5-45e2-ac17-42d089e3c3f8.png" />
</picture>
用户设置
应用应该允许用户主动去选择外观模式,设置后外观模式将不再跟随系统设置。
具体实现的核心代码如下:
function getThemeMode () {
return localStorage.getItem('THEME_MODE')
}
function setThemeMode (value) {
localStorage.setItem('THEME_MODE', value)
}
export default new Vuex.Store({
state: {
themeMode: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
},
getters: {
themeMode (state) {
return getThemeMode() || state.themeMode
}
},
mutations: {
SET_THEME_MODE (state, mode) {
state.themeMode = mode
}
}
})
在根节点上添加 theme-mode 属性,使应用初次加载时,也可渲染当前设置的外观模式效果。
<template>
<div id="app" :theme-mode="themeMode"></div>
</template>
<script>
export default {
computed: {
themeMode () {
return this.$store.getters.themeMode
}
},
mounted () {
window.matchMedia('(prefers-color-scheme: dark)').addListener((e) => {
this.$store.commit('SET_THEME_MODE', e.matches ? 'dark' : 'light')
})
},
}
</script>
:root {
--white: #fff;
--gray-1: #4E4C56;
--gray-2: #f8f8f8;
--dark-1: #292934;
--dark-2: #23232B;
}
.theme-mode-light {
--text-color: var(--gray-1);
--background-color: var(--gray-2);
--card-background-color: var(--white);
}
.theme-mode-dark {
--text-color: var(--white);
--background-color: var(--dark-2);
--card-background-color: var(--dark-1);
}
#app[theme-mode=light] {
&:extend(.theme-mode-light);
}
#app[theme-mode=dark] {
&:extend(.theme-mode-dark);
}
@media (prefers-color-scheme: light) {
:root {
&:extend(.theme-mode-light);
}
}
@media (prefers-color-scheme: dark) {
:root {
&:extend(.theme-mode-dark);
}
}
总结
本文介绍了深色模式适配方案和项目中会遇到的问题。如果有错误的地方或者有更好的解决想法,欢迎留言讨论。