前置知识

在这里了解实现网页主题切换的相关知识。

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()函数

  1. 通过 var() 函数读取变量。例如:var(--example) 会返回 --example所对应的值。

  2. 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 媒体特性作介绍,参考MDNprefers-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主题适配方案指北的更多相关文章

  1. 移动web开发适配方案之Rem

    移动端为什么要做适配 移动端相对PC端来说大部分浏览器内核都是基于Webkit的,所以大部分都支持CSS3的最新语法.但是由于手机的屏幕尺寸和分辨率都不太一样(尤其是安卓),所以不得不对不同分辨率的手 ...

  2. 移动web屏幕适配方案

    刚进部门就被拉去趟移动端Web的浑水,视觉稿是按照640px设计的.那如何做屏幕适配呢?当然想到的第一方法就是问前辈了,问他们之前怎么做的,前辈说直接按视觉稿来,我说640太大了,他说除以2啊,按32 ...

  3. 移动端Web页面适配方案

    概念理解 viewport视口 visual viewport 可见视口,设备屏幕的宽度  windw.innerWidth/Height layout viewport 布局视口,DOM宽度 doc ...

  4. 移动端 移动web屏幕适配方案 随不同宽度的屏幕而改变

    链接地址1:http://www.cnblogs.com/zjzhome/p/4802157.html 链接地址2:http://www.html-js.com/article/Mobile-term ...

  5. 再谈移动端Web屏幕适配

    一个多月前水了一篇移动web屏幕适配方案,当时噼里啪啦的写了一通,自我感觉甚是良好.不过最近又有一些新的想法,和之前的有一些不同. 先说一下淘宝的方案,感觉现在好多的适配方案都是受了它的影响,上周六看 ...

  6. Web 端屏幕适配方案

    基础知识 像素相关 1.像素 :像素是屏幕显示最小的单位. 2.设备像素 :设备像素又称物理像素(physical pixel),设备能控制显示的最小单位,我们可以把这些像素看作成显示器上一个个的点. ...

  7. WEB安全漏洞挖掘向入坑指北

    这个指北不会给出太多的网站和方向建议,因为博主相信读者能够从一个点从而了解全局,初期的时候就丢一大堆安全网址导航只会浇灭人的热情,而且我也不适合传道授业解惑hhh 安全论坛: 先知社区 freebuf ...

  8. [转] iOS开发者的Weex伪最佳实践指北

    [From] http://www.cocoachina.com/ios/20170601/19404.html 引子 这篇文章是笔者近期关于Weex在iOS端的一些研究和实践心得,和大家一起分享分享 ...

  9. webapp:移动端高清、多屏适配方案(zz)

    来源: http://sentsin.com/web/1212.html 移动端高清.多屏适配方案 背景 开发移动端H5页面 面对不同分辨率的手机 面对不同屏幕尺寸的手机 视觉稿 在前端开发之前,视觉 ...

随机推荐

  1. 自动化不知如何参数化?xlrd来帮你解决

    平时在做自动化测试的时候,一直都是要求数据与业务逻辑分离.把测试数据都写在业务里面的话,比较混杂.为了方便管理测试数据,所以引入了python的一个扩展库--xlrd.该库使用简单,能满足自动化测试的 ...

  2. Salt 系统初始化

    目录 编辑states文件 1.DNS配置  dns.sls(在init目录下创建一个files文件,然后把resolv.conf放到文件下) [root@master init]# cat dns. ...

  3. Mysql 的数据导入导出

    一. mysqldump工具基本用法,不适用于大数据备份   1. 备份所有数据库: mysqldump -u root -p --all-databases > all_database_sq ...

  4. 老男孩Django笔记(非原创)

    .WEB框架 MVC Model View Controller 数据库 模板文件 业务处理 MTV Model Template View 数据库 模板文件 业务处理 ############## ...

  5. PHP quoted_printable_decode() 函数

    实例 对经过 quoted-printable 编码后的字符串进行解码,返回 8 位的 ASCII 字符串: <?php高佣联盟 www.cgewang.com$str = "Hell ...

  6. luogu P4775 [NOI2018]情报中心 线段树合并 虚树 树的直径trick

    LINK:情报中心 神题! 写了一下午 写到肚子疼. 调了一晚上 调到ex 用的是网上dalao的方法 跑的挺快的. 对于链的暴力 我不太会kk. 直接说正解吧: 分类讨论两种情况: 1 答案的两条链 ...

  7. .NetCore 入门

    .net core是什么? .net core是一个可以用来构建现代.可伸缩和高性能的跨平台软件应用程序的通用开发框架. 我们为什么要使用.net core,也就是说.net core有什么好处? 跨 ...

  8. 一、elasticsearch部署

    Elasticsearch官网: https://www.elastic.co/products/elasticsearch 一.Linux单节点部署 1. 解压elasticsearch-5.6.1 ...

  9. 浅谈Mybatis持久化框架在Spring、SSM、SpringBoot整合的演进及简化过程

    前言 最近开始了SpringBoot相关知识的学习,作为为目前比较流行.用的比较广的Spring框架,是每一个Java学习者及从业者都会接触到一个知识点.作为Spring框架项目,肯定少不了与数据库持 ...

  10. JQuery插件,轻量级表单模型验证(续 一)

    之前的代码结构,不方便扩展多结构的模型验证 重新结构设计了一下验证模型核心 var validateForm = (function(model) { model.Key = "[data- ...