一文搞懂前端多主题适配方案自 IOS 13.0 支持深色模式(DarkMode)后,多主题逐渐流行起来,用户可以手动或自
多主题介绍
自 IOS 13.0 支持深色模式 (DarkMode) 后,多主题逐渐流行起来,用户可以手动或自动切换到自己喜欢的主题色,本篇文章详细介绍下前端页面如何实现多主题适配。
Mac 系统支持的浅色和深色主题示意图:
在具体实施多主题方案之前,首先要了解两个问题
⭐️ 为什么会出现深色模式?
- 在视力正常 (或矫正视力正常) 的人群中,浅色模式的视觉表现往往更好,而一些患有白内障和相关疾病的人可能在深色模式下表现更好。另一方面,在光模式下长期阅读可能与近视有关。
- 日常需要长时间使用的应用,深色模式会减轻眼部疲劳。
⭐️ 我应该去实施深色主题的适配吗?
相关研究调查数据显示,超过 80% (在 Mac 上,而不是在 iOS 上,但这给出了一个趋势),尽管这个数字可能被高估了,但这意味着有很大比例的人在使用它。所以对于使用深色模式的用户来说,切换到不支持的应用真的很不爽,他们很可能会为此删除掉应用或关掉页面。
多主题成为用户体验提升基本手段,现在化前端都应该支持此场景,接下来会深入讲解多主题在 WEB 场景中如何适配。
多主题技术方案探索
浏览器基于操作系统支持浅色深色两种风格,但是前端页面的多主题需要开发者具体开发才可以实现主题切换。
操作系统支持情况
做具体实现之前,了解下系统层次支持暗黑模式的支持情况:
- macOS 10.14 引入了 darkmode
- ios13 2019 年 3 月发布的 ios13 版本加入了 darkmode
- Android 10 (API 级别 29) 及更高版本中提供深色主题背景
- window10 2018.10.10
在现代化设备中,系统层都支持了深色和浅色主题,所以无论是新项目还是已经跑了多年的老项目都应该去做这件事。
自定义样式适配思路
页面基本是由自定义样式 + UI库来呈现整体视觉。 先从自定义样式说起:自定义样式使用CSS变量控制多主题(CSS变量基本使用在此不再过多赘述,入门请参考阮一峰老师写的CSS 变量教程)
<!-- html 节点添加主题自定义属性 -->
<html data-theme="light">
<!-- 使用CSS变量控制样式 -->
<body style="background: var(--body-background)"></body>
</html>
// 跟主题无关的变量放到root里
:root {
--border-radius-base: 6px;
}
// 跟主题相关变量,通过属性选择器提升优先级
html[data-theme='default']:root {
--body-background: #efefef;
}
html[data-theme='dark']:root {
--body-background: #000;
}
UI库适配思路
开源社区提供的UI库主题适配方案比较流行的就是通过CSS变量,但是由于CSS变量形成规范较晚,所以较老的项目是不支持的。Vue 的 Element UI 和 React 的 Ant Design 是社区发展较好的两个 UI 库,因为项目较老的原因都不支持CSS变量主题方案。
拿Ant Design来说,内部实现多主题是通过定义 less 变量,这样的做法是无法动态切换主题(下面会具体讨论动态切换主题)
🎉 Ant Design 目前正在着手将所有组件支持CSS变量,详细进展可以参考这里。
但是我们不能等官方支持后在着手做适配工作,需求不等人,以 antd 为例,适配浅色和深色主题目前可以通过分别构建不同主题样式,通过添加属性选择器前缀来控制antd样式的优先级,以达到适配主题的目的。
<html data-theme="light"></html>
html[data-theme='light'] .ant-button {color: #fff}
html[data-theme='dark'] .ant-button {color: #000}
动态切换主题思路
页面切换主题具体需要从下面三个维度来考虑:
- 系统主题更换
- 页面提供主题切换按钮,用户主动切换
- 通过URL控制当前主题
切换主题的核心思路是通过控制CSS变量,在不同主题下显示不同的样式。
⭐️ 系统主题切换
浏览器暴露主题切换接口:
CSS | 媒体查询 @media(prefers-color-scheme: dark) |
---|---|
JavaScript | window.matchMedia("(prefers-color-scheme: dark)") |
api支持程度:
⭐️⭐️ 通过CSS媒体查询控制CSS变量:
body {
background: var(--body-background);
transition: background 0.3s;
}
@media (prefers-color-scheme: light) {
:root {
--body-background: #efefef;
--text-color: #333;
}
}
@media (prefers-color-scheme: dark) {
:root {
--body-background: #000;
--text-color: #ededed;
}
}
优点是实现简单,识别交给浏览器去做,简单页面直接使用这个方案即可。
缺点是不利于扩展,后续支持用户主动切换主题比较乏力,所以我们下面按照属性选择器加CSS变量实现控制。
⭐️⭐️ 通过JS暴露接口:
通过JS识别当前系统主题,对于CSS变量的控制不应该使用JS去写,因为当主题色多的话,需要对每个属性都执行这行代码:
document.documentElement.style.setProperty('--theme-color', '#YOURCOLOR');
JS控制CSS变量不利于后续扩展。
较合适的方案是上面提到的:通过属性选择器控制根节点CSS变量
/* 浅色模式 */
html[data-theme="light"]:root {
--body-background: #efefef;
--text-color: #333;
}
/* 深色模式 */
html[data-theme="dark"]:root {
--body-background: #000;
--text-color: #ededed;
}
JS通过识别系统主题,设置页面主题标识
// 给HTML DOM节点添加自定义主题,标识当前主题
const toggleTheme = (isDarkMode) => {
htmlEl.setAttribute("data-theme", isDarkMode ? "dark" : "light");
};
const themeMedia = window.matchMedia("(prefers-color-scheme: dark)");
// 页面初始化切换
toggleTheme(themeMedia.matches);
// 监听系统切换
themeMedia.addListener((e) => {
toggleTheme(e.matches);
});
⭐️ 页面提供切换按钮,用户主动切换
需要在html节点添加自定义属性,并根据当前主题色通过CSS变量控制。
点击切换按钮后设置自定义属性值即可。
/* 浅色模式 */
html[data-theme="light"]:root {
--body-background: #efefef;
}
/* 深色模式 */
html[data-theme="dark"]:root {
--body-background: #000;
}
body {
background: var(--body-background);
transition: background 0.3s;
}
const htmlEl = document.documentElement;
const buttonEl = document.getElementById("btn");
buttonEl.addEventListener("click", () => {
const currentTheme = htmlEl.getAttribute("data-theme");
const nextTheme = currentTheme === "dark" ? "light" : "dark";
htmlEl.setAttribute("data-theme", nextTheme);
});
⭐️ 通过URL控制当前主题
页面加载后根据URL query参数动态添加到html节点自定义属性,并根据当前主题色通过CSS变量控制。
const search = new URLSearchParams(location.search);
const theme = search.get("theme") || "light";
document.documentElement.setAttribute("data-theme", theme);
上面三种场景结合起来,基本满足目前多主题所有需求。
在线DEMO: codesandbox.io/s/multiple-…
多主题方案实现
如果项目中没有使用UI库或使用了支持CSS变量主题适配的UI库,可以本章内容,请直接参考上面技术方案探索。
根据上面「多主题技术方案探索」结论,我们从「UI库适配」和「自定义样式适配」来介绍下页面如何从0到1实现多主题适配。
先放一张最终实现效果图,源码已放到 github 仓库,有条件的看官可以边看源码,边阅读下面的示例代码
在线演示:multiple-theme.vercel.app/
UI库主题适配实现
基于Ant Design具体讲UI库如何实现多主题方案。对于其他不支持的UI库(Element UI等等),实现的思路大致相同。
共分为三个步骤:
- 配置多主题样式
- 重写组件样式
- 构建样式文件
通过配置深色和浅色两套主题文件,分别对需要自定义样式的less变量赋值。最终构建两套主题样式,分别加上属性选择器前缀。
# 文件目录结构
.
├── src
│ └── theme
│ ├── antd.css # 构建产物
│ ├── antd.custom.variable.less # 自定义antd less变量
│ ├── antd.dark.base.less # 自定义 antd 深色主题
│ ├── antd.light.base.less # 自定义 antd 浅色主题
│ ├── antd.less # 构建入口文件
└── theme.sh
构建两套样式无疑增加了构建产物大小,最终构建两套样式文件大小约为1.4M,gzip后300K左右。
如果你的项目是刚启动并且比较小,可以使用支持CSS变量的UI库。
如果项目预期会很庞大,而且希望使用antd周边生态比较完善的库,那么我建议你做这件事情。
⭐️ 配置多主题样式
antd.custom.variable.less
自定义antd less变量文件
为什么要单独设置一份antd的less变量设置,由于上面提到过的:antd作为老项目基本都支持CSS变量,主题方案是通过less变量实现的,所以我们要自己设置不同主题的less变量。
@live-primary-color: #ff0040;
// default
@live-body-background: #efefef;
@live-text-color: #666;
// dark
@live-body-background-dark: #000;
@live-text-color-dark: #bfbfbf;
antd.dark.base.less
自定义 antd 深色主题
@import './antd.custom.variable.less';
@import '../../node_modules/antd/lib/style/themes/dark.less';
@import '../../node_modules/antd/dist/antd.less';
// 覆盖antd less变量值
@primary-color: @live-primary-color;
@body-background: @live-body-background-dark;
antd.light.base.less
自定义 antd 浅色主题
@import "./antd.custom.variable.less";
@import "../../node_modules/antd/lib/style/themes/default.less";
@import "../../node_modules/antd/dist/antd.less";
// 覆盖antd less变量值
@primary-color: @live-primary-color;
@body-background: @live-body-background;
构建:分别构建浅色和深色主题
# '编译暗黑主题样式'
npx lessc --js ./src/theme/antd.dark.base.less ./src/theme/antd.dark.base.css
# '编译白色主题样式'
npx lessc --js ./src/theme/antd.default.base.less ./src/theme/antd.default.base.css
构建产物:antd.dark.base.css
antd.default.base.css
⭐️ 自定义UI库样式
variable.css
自定义CSS变量
此文件用于自定义样式和UI库样式的CSS变量合集
:root {
--live-primary-color: #ff0040;
--live-font-size-base: 14px;
--live-font-size-lg: 16px;
--live-font-size-sm: 12px;
}
html[data-theme="default"]:root {
--live-body-background: #efefef;
}
html[data-theme="dark"]:root {
--live-body-background: #000;
}
ant.less
自定义UI库样式文件
@import (less) "./variable.css";
html[data-theme="default"] {
@import (less) "./antd.default.base.css";
}
html[data-theme="dark"] {
@import (less) "./antd.dark.base.css";
}
html[data-theme="dark"],
html[data-theme="default"] {
.ant-btn {
font-size: var(--font-size-lg);
&-lg {
font-size: 18px;
}
&-sm {
font-size: var(--font-size-base);
}
}
}
构建:给浅色和深色分别添加作用域,并加入自定义UI样式。
因为我们整体的切换主题要结合html标签的自定义属性 data-theme
的值为 light
或 dark
来实现,所以要结合构建的浅色和深色的的两份主题文件添加作用域。
npx lessc --js -clean-css ./src/theme/antd.less ./src/theme/antd.css
构建产物: antd.css
,在项目head标签中引入,就完成了UI库的主题适配。
自定义样式主题适配实现
因为「UI库主题适配实现」中,variable.css
设置了CSS变量,所以自定义样式的节点上直接使用var(CSS变量)即可。
<div style="color: var(--text-color)"></div>
总结
本篇文章重点讲「不支多主题的UI库」适配,相信未来所有的UI库都会支持CSS变量实现多主题切换,目前antd正在做这件事,等完全支持后,多主题适配需要关心的是自定义UI库样式和自定义样式主题适配。
如果你有更好的主题适配方案,欢迎一起讨论。
转载自:https://juejin.cn/post/7049384448256639006