web主题适配方案指北
前置知识
在这里了解实现网页主题切换的相关知识。
CSS 变量
要实现主题切换需要了解一点 css 自定义属性。当然,本文还提供了其他实现方式,为了不给您接下来的阅读带来阻碍,先了解它。
变量的声明
声明变量时,变量名前要加上 --,例如 --example: 20px 即是一个 css 声明语句。意思是将 20px 赋值给 --example 变量。所以 css 变量又叫做 css 自定义属性。在 css 的任何选择器中都可以声明 css 变量,通常将所有 css 变量声明在 :root 选择器中,以便在后文引用。
:root选择器匹配文档树的根元素。对于 HTML 文档来说,:root表示<html>元素,除了优先级更高之外,与 html 选择器相同。
这里有一个例子:
:root {
--example: 20px
}
等价于:
html {
--example: 20px
}
var()函数
通过
var()函数读取变量。例如:var(--example)会返回--example所对应的值。var()函数还可以使用第二个参数,表示变量的默认值。即var()从左向右读取值,如果第一个变量不存在,就读取第二个。例如:var(--example, 40px), 如果--example不存在,将返回 40px。当然第二个参数同样可以使用 css 自定义属性而不是具体的值,例如:var(--example1, --example2)。
试着写一个简单的例子:
<body>
<section id="container">
<div class="item1"></div>
<div class="item2"></div>
<div class="item3"></div>
<div class="item4"></div>
</section>
</body>
#container {
width: 400px;
height: 150px;
background-color: #ffeead;
border: 1px solid #666;
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
}
#container > div {
width: 70px;
height: 50px;
}
#container div:nth-child(2n) {
background-color: lightgreen;
}
#container div:nth-child(2n+1) {
background-color: lightpink;
}

接下来使用 css 变量,修改部分代码:
+ :root {
+ --green: lightgreen;
+ --lightpink: lightpink;
+ }
#container {
width: 400px;
height: 150px;
background-color: #ffeead;
border: 1px solid #666;
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
}
#container > div {
width: 70px;
height: 50px;
}
#container div:nth-child(2n) {
- background-color: lightgreen;
+ background-color: var(--green);
}
#container div:nth-child(2n+1) {
- background-color: lightpink;
+ background-color: var(--lightpink);
}

在上面的代码片段中,使用 css 变量替换原来的颜色值,效果依然相同。css 变量还有许多其他相关知识,本文只介绍这些内容,只需要掌握这些,就能实现完整的暗色模式了。
兼容性

由图可见,如果不需要兼容 IE 浏览器,可以放心使用它。要兼容 IE 也有办法,postcss-css-variables 插件将 CSS 自定义属性 (CSS 变量) 语法转换为静态表示形式,具体使用方式本文不展开了,点我 查看详细的官方使用教程。
跟随系统设置
使用 css 媒体查询匹配系统设置。此处简单将 prefers-color-scheme CSS 媒体特性作介绍,参考MDN。prefers-color-scheme用于检测用户是否有将系统的主题色设置为亮色或者暗色。
// 表示系统未得知用户在这方面的选项
@media (prefers-color-scheme: light) { }
// 用户选择选择使用浅色主题的系统界面
@media (prefers-color-scheme: dark) { }
// 用户选择选择使用暗色主题的系统界面
@media (prefers-color-scheme: no-preference) { }
使用 matchedMedia() 匹配系统设置。
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
if (prefersDarkScheme.matches) {
document.body.classList.add('dark-theme');
} else {
document.body.classList.remove('dark-theme');
}
使用这种方式有一个缺点:当这段 JavaScript 在 CSS 之后执行时,可能会出现一个闪屏的现象。
实现方式
有多种方式实现暗色模式,逐一介绍。
使用 css 变量
利用 var() 函数的特性实现浅色模式和暗色模式之间的切换。看下面的代码片段:
:root{
--default-color: #555,
--color: var(--dark-var, --default-var)
}
body{
color: var(--color)
}
body 最终得到的 color 为 #555 ,如果声明了 —-dark-var 变量,body 得到的 color 的值将为 —-dark-var 的值,可以通过 JavaScript 将变量 —-dark-var 插入到 css 。
结合媒体查询实现跟随系统切换到深色模式:
@media (prefers-color-scheme: dark) {
:root {
--dark-color: #fff
}
}
使用 html 属性和 css 变量
给 html 标签动态添加 theme 属性,像这样 <html theme="dark"> 。
:root{
--color: #555
}
:root[theme="dark"]{
--color: #fff
}
body {
color: var(--color)
}
当 html 的属性 theme 的值不为 dark 时,var() 函数读取的是 :root{} 内的自定义属性,反之,则读取的是 :root[theme="dark"]{} 里的自定义属性。
同样也可以结合媒体查询,实现跟随系统的效果:
@media (prefers-color-scheme: dark) {
:root {
--color: #fff
}
}
这种方式的好处是扩展性更强,代码量更少,代码维护也更加方便。不仅仅可以切换到深色模式,还可以切换到其他主题。例如给 html 的 theme 属性设置其他值 <html theme="pink"> ,只需要添加下面这段 css:
:root[theme="pink"]{
--color: pink
// ...
}
通过 JavaScript 给 html 的 theme 属性赋值 pink ,就能切换到该主题了。
使用 class 和 css 变量
类似的思路我们可以给 html 添加一个 class 而不是添加 theme 属性实现。
::root{
--color: #222;
}
:root.dark{
--color: #eee;
}
const btn = document.querySelector('.toggle');
btn.addEventListener('click', function() {
document.html.classList.toggle('dark');
})
用户设置深色模式的操作系统并不意味着他们希望将深色模式应用到网站上。结合媒体查询还可以实现:不管系统设置如何,覆盖深色模式。
body {
--color: #222;
}
body.dark{
--color: #eee;
}
@media (prefers-color-scheme: dark) {
body {
--color: #eee;
}
body.light {*
*--color: #222;
}
}
使用 class 作为标识
通过 JavaScript 改变 body 上的 class 来决定网站使用的主题。
<body class="dark || light">
const btn = document.querySelector('.toggle');
btn.addEventListener('click', function() {
document.body.classList.toggle('dark');
})
body {
color: #222;
background: #fff;
}
body.dark{
color: #eee;
background: #121212;
}
使用这种方式时,我推荐使用 css 预处理器比如 scss,不然需要做很多重复的劳动。
使用单独的 css 文件
light-theme.css
body {
color: #222;
background: #fff;
}
dark-theme.css
body {
color: #eee;
background: #121212;
}
这时候你可能会有疑问了,如何通过点击切换主题呢?不用担心,非常简单。在引入 css 时这样做:
<head>
<link href="light-theme.css" rel="stylesheet" id="theme-link">
</head>
给link 一个标签一个 ID, 就可以通过 JavaScript 选择它了。
const btn = document.querySelector(".toggle");
const theme = document.querySelector("#theme-link");
btn.addEventListener("click", function() {
if (theme.getAttribute("href") == "light-theme.css") {
theme.href = "dark-theme.css";
} else {
theme.href = "light-theme.css";
}
});
使用 Darkmode.js
GitHub 上有一个开源项目,Darkmode.js,通过mix-blend-mode:difference 达到切换夜间模式的效果,当您的应用或网站比较简单时或许可以尝试它。
npm install darkmode-js
const options = {
bottom: '64px',
right: 'unset',
left: '32px',
time: '0.5s',
mixColor: '#fff',
backgroundColor: '#fff',
buttonColorDark: '#100f2c',
buttonColorLight: '#fff',
saveInCookies: false,
label: '',
autoMatchOsTheme: true
}
const darkmode = new Darkmode(options);
darkmode.showWidget();

具体的原理我已经写了另一篇文章 Darkmode.js 源码解析 介绍它。
使用服务端脚本
以 PHP 为例:
<?php
$themeClass = '';
if (isset($_GET['theme']) && $_GET['theme'] == 'dark') {
$themeClass = 'dark-theme';
}
$themeToggle = ($themeClass == 'dark-theme') ? 'light' : 'dark';
?>
<!DOCTYPE html>
<html lang="en">
<!-- etc. -->
<body class="<?php echo $themeClass; ?>">
<a href="?theme=<?php echo $themeToggle; ?>">Toggle Dark Mode</a>
<!-- etc. -->
</body>
</html>
实现切换单独的 css 文件:
<?php
$themeStyleSheet = 'light-theme.css';
if (isset($_GET['theme']) && $_GET['theme'] == 'dark') {
$themeStyleSheet = 'dark-theme.css';
}
$themeToggle = ($themeStyleSheet == 'dark-theme.css') ? 'light' : 'dark';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<!-- etc. -->
<link href="<?php echo $themeStyleSheet; ?>" rel="stylesheet">
</head>
<body>
<a href="?theme=<?php echo $themeToggle; ?>">Toggle Dark Mode</a>
<!-- etc. -->
</body>
</html>
这种方法有一个明显的缺点:需要刷新页面才能进行切换。但是,像这样的服务器端解决方案对于跨页面重新加载持久化用户的主题选择非常有用。
选择哪种方法
如果不需要兼容 IE,我推荐您使用 html 属性和 css 变量。或者,将不同方式结合使用也未尝不可。总之,应该根据您的项目需求选用更加合适的方法。
细节处理
图片
大多数网站不仅仅只有文本,还有图片。使用 filter 来处理图片。
img.dark{
filter: brightness(.8) contrast(1.2);
}
brightness 使图像看起来更亮或更暗
contrast 调整图像的对比度

阴影
不要忽略暗色模式下的阴影(box-shadow),浅色模式的阴影在深色模式下可能无法显现。应该将这些细节也抽分,即应该为深色模式也提供一套阴影。
css 变量粒度
在抽离或者定义 css 变量时应该尽可能掌控粒度。粒度太大不好掌控细节,太小会增加代码量,不易维护。总之,视情况而定还有您的经验。
过渡效果
不要使用 transition
从暗色模式切换到浅色模式,或者从浅色模式切换到暗色模式,需要一个过渡动效,这能改善体验。您或许立即想到了transition,千万不要这么做。下面来看看这样做时有什么不足。
如果利用 css transition 属性,在您切换模式时应该给顶层元素一个 class,例如 mode-change ,在切换完成之后再将它移除。scss 代码大至如下:
.mode-change {
selector1,
selector2,
// ......{
transition: all 0.3s cubic-bezier(1, 0.05, 0.29, 0.99);
}
}
这是因为:使用 transition,您需要给所有元素添加过渡效果。这将带来巨大的 CPU 开销,我已经测试过了。
障眼法
如果您常写 css 特效,障眼法真是个巧妙的方法,利用它实现很多难以实现的效果。现在,甚至用它来优化性能。不妨回归最初,目的是什么?给用户一个过渡效果,看起来不那么生硬。思路:使用 css 伪元素创建一个带有过渡效果的蒙层,看看如和实现它吧。
在切换时给 body 添加一个 class,在浅色切换到深色时 class 为 light-to-dark,反之,为 dark-to-light。
代码较长,点击展开
$mode: () !default;
$mode: map-merge(
(
light-bg: #fff,
dark-bg: #252528,
),
$mode
);
$light-bg: map-get($mode, light-bg);
$dark-bg: map-get($mode, dark-bg);
.dark-to-light:after {
content: '';
width: 100vw;
height: 100vh;
position: fixed;
z-index: 99999;
left: 0;
top: 0;
margin-left: 0;
background-color: $dark-bg;
opacity: 0.7;
animation: toLight 1s linear 0s forwards;
// pointer-events: none;
}
.light-to-dark:after {
content: '';
width: 100vw;
height: 100vh;
position: fixed;
z-index: 99999;
left: 0;
top: 0;
margin-left: 0;
background-color: $light-bg;
opacity: 0.7;
animation: toDark 1s linear 0s forwards;
// pointer-events: none;
}
@keyframes toLight {
0% {
background-color: $dark-bg;
opacity: 0.7;
}
100% {
background-color: $light-bg;
opacity: 0;
}
}
@keyframes toDark {
0% {
background-color: $light-bg;
opacity: 0.7;
}
100% {
background-color: $dark-bg;
opacity: 0;
}
}
在切换模式时,将会在页面顶层展示带有对应过渡效果的蒙层。在过渡效果显示时,用户的鼠标无法点击页面的元素,这样做实现了类似防抖的效果。如果想移除这个效果,只需给蒙层加上 pointer-events: none; 。最终的效果就是:即使频繁切换模式,也听不见 CPU 狂飙。

储存状态
仅仅通过点击按钮切换主题还不够,应该将主题保存起来。否则,用户刷新页面或者再此进入页面将回到初始主题。在切换主题或者初始化时都应该使用状态储存。
JavaScript LocalStorage
在切换皮肤时应该将当前主题存到 LocalStorage。
localStorage.setItem("theme", "dark" || "light");
localStorage.getItem("theme");
PHP Cookie
$_COOKIE['theme'] == 'dark'
暗色模式设计最佳实践
我对设计和色彩不够专业,我找了一篇优秀的文章,您可以点此查看。
参考资料:
web主题适配方案指北的更多相关文章
- 移动web开发适配方案之Rem
移动端为什么要做适配 移动端相对PC端来说大部分浏览器内核都是基于Webkit的,所以大部分都支持CSS3的最新语法.但是由于手机的屏幕尺寸和分辨率都不太一样(尤其是安卓),所以不得不对不同分辨率的手 ...
- 移动web屏幕适配方案
刚进部门就被拉去趟移动端Web的浑水,视觉稿是按照640px设计的.那如何做屏幕适配呢?当然想到的第一方法就是问前辈了,问他们之前怎么做的,前辈说直接按视觉稿来,我说640太大了,他说除以2啊,按32 ...
- 移动端Web页面适配方案
概念理解 viewport视口 visual viewport 可见视口,设备屏幕的宽度 windw.innerWidth/Height layout viewport 布局视口,DOM宽度 doc ...
- 移动端 移动web屏幕适配方案 随不同宽度的屏幕而改变
链接地址1:http://www.cnblogs.com/zjzhome/p/4802157.html 链接地址2:http://www.html-js.com/article/Mobile-term ...
- 再谈移动端Web屏幕适配
一个多月前水了一篇移动web屏幕适配方案,当时噼里啪啦的写了一通,自我感觉甚是良好.不过最近又有一些新的想法,和之前的有一些不同. 先说一下淘宝的方案,感觉现在好多的适配方案都是受了它的影响,上周六看 ...
- Web 端屏幕适配方案
基础知识 像素相关 1.像素 :像素是屏幕显示最小的单位. 2.设备像素 :设备像素又称物理像素(physical pixel),设备能控制显示的最小单位,我们可以把这些像素看作成显示器上一个个的点. ...
- WEB安全漏洞挖掘向入坑指北
这个指北不会给出太多的网站和方向建议,因为博主相信读者能够从一个点从而了解全局,初期的时候就丢一大堆安全网址导航只会浇灭人的热情,而且我也不适合传道授业解惑hhh 安全论坛: 先知社区 freebuf ...
- [转] iOS开发者的Weex伪最佳实践指北
[From] http://www.cocoachina.com/ios/20170601/19404.html 引子 这篇文章是笔者近期关于Weex在iOS端的一些研究和实践心得,和大家一起分享分享 ...
- webapp:移动端高清、多屏适配方案(zz)
来源: http://sentsin.com/web/1212.html 移动端高清.多屏适配方案 背景 开发移动端H5页面 面对不同分辨率的手机 面对不同屏幕尺寸的手机 视觉稿 在前端开发之前,视觉 ...
随机推荐
- javascript 通信协议
简介 javascript 通信协议是一个伪协议[1], 用于指定 URL 为 JavaScript 代码 语法: javascript:someScript; someScript 是一个或多个使用 ...
- Java基础之(IO流)
简介: 流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象.即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作. 一.File ...
- ElasticSearch(三)springboot整合ES
最基础的整合: 一.maven依赖 <parent> <groupId>org.springframework.boot</groupId> <artifac ...
- w10查看wifi密码
1.选择网络和Internet设置 右键单击网络连接图标,选择“打开网络和Internet设置”. 2.选择网络和共享中心
- 安装 kreas 2.2.4 版本问题
python 版本 3.6 不要使用 3.7 和 3.8 否则导致安装失败 tensorflow 使用 1.12.0 keras 使用 2.2.4 版本 pip install 包名 -i http: ...
- PHP strtotime() 函数
------------恢复内容开始------------ 实例 将任何字符串的日期时间描述解析为 Unix 时间戳: <?php // 设置时区 date_default_timezone_ ...
- BSOJ 5445 -- 【2018雅礼】树 prufer序列 dp
BSOJ在哪我也不知道 没有链接. 对于有标号无根树的统计和有度数限制 一般采用prufer序列. 根据prufer序列 容易知道 某个点的出现次数+1为当前点的度数. 对于这道题 考虑设f[i][j ...
- 关于ORACLE索引的几种扫描方式
------------恢复内容开始------------ ------------恢复内容开始------------ 一条sql执行的效率因执行计划的差异而影响,经常说这条sql走索引了,那条s ...
- Java并发学习(一):进程和线程
好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航 前言 俗话说得好"一人 ...
- 嵌入式linux简介
嵌入式linux系统应用非常广泛,涵盖各行各业,基于ARM.mips等微处理器架构的硬件平台.基于嵌入式linux系统的设备已经深入生活中各个角落,随处可见. 我们常说的嵌入式linux系统,其实 ...