如何使用CSS实现黑暗模式和高亮模式的切换

发布于 大漠

Web技巧第五期中专门提到一个有关于CSS实现黑暗模式和高亮模式的技术方案。即使用新的媒体查询条件prefers-color-scheme的值darklight来进行切换,这是从最底层也是最原生的解决方案,除此之外还可以通过CSS的混合模式属性来模拟。当然,除了期刊中提到的技术方案之外,还有其他的一些解决方案。今天我们就来一起学习一下,如何实现黑暗模式和高亮模式之间的切换。

什么是黑暗模式和高亮模式

在聊技术方案之前先来简单地了解什么是黑暗模式和高亮模式?这两个概念是来源于macOS系统,该系统为用户提供两个主题皮肤,即高亮暗色 系的皮肤。自从有了这个概念之后,很多网站都会用户提供了相应的两套肤色,便于用户根据自己的习惯或爱好进行切换。

不管是黑暗模式还是高亮模式,都是黑白色之间的切换,这种主题风格对于有色盲的用户群体而言是非常有友好的。

类似这样的功能,在其他的系统或者软件中都略有身影,不同之处是提供的模式。在一些软件中,可能会给用户提供一些皮肤自定制的功能。当然在网站上也有类似的功能,只不过我们以往可能更喜欢把这种功能称为 网站换肤

这样一来,我们就可以把这两者模式之间的切换先按换肤来聊,可能会更切合我们的业务场景。接下来我们来聊聊技术上面的事情,即 如何使用CSS来完成Web页面或应用程序的主题切换!

最简模式

假设你的主题默认是高亮模式,我们可以使用一种最为简单粗暴的方式将高亮模式切换到黑暗模式。假设在Web页面有一个入口,让用户点击这个button时会给html元素添加一个dark-theme类名:

document.getElementById('buttonID').addEventListener('click', function(){
    document.documentElement.classList.add('dark-theme')
})

.dark-theme以及他所有后代元素上添加暗黑色样式:

.dark-theme {
    background-color: #000;
    color: white;
}

.dark-theme *:not(a) {
    background-color: #000 !important;
    color: #fff !important;
    border-color: #999 !important;
}

这种方式虽为简单粗暴,但有些细节需要额外处理,特别是代码中也有使用!important的样式时,会较为头痛。另外对于其他涉及到颜色的元素有可能需要额外处理。

准备两套样式

在我个人的印象之中,最早实现类似的效果,一般都是通过JavaScript来更换Web页面或Web应用程序主题皮肤的.css文件:

正如上图所示,提供了两个CSS文件,一个是theme1.css,另一个是theme2.css,同时提供用户可切换的入口,当用户选择对应的主题之后,Web页面或Web应用程序就会切换到相应的.css,从而看到的就是对应的主题肤色效果。

假设Web页面默认的主题风格是运用的theme1.css

<link type="text/css" rel="stylesheet" media="all" href="../theme1.css" id="theme_css" />

在代码中提供一个简单的脚本函数:

document.getElementById('buttonID').addEventListener('click', function(){
    document.getElementById('theme_css').href = '../theme2.css';
})

回到我们主题中来,如果你需要黑暗模式和高亮模式之间的切换,那可以按类似的原理,分别提供dark.csslight.css

对于维护多套样式是较为痛苦的,特别当你要为你的产品提供更多的皮肤的时候更为堪忧。这个时候你可以借助类似Sass这样的处理器来维护你的主题样式,声明好变量,然后维护对应的变量值,好比Bootstrap主题的构建一样,他就使用了Sass的变量

有关于Sass来管理多套皮肤相关的知识已超出本文要探讨的范围,如果你对这方面知识感兴趣的话,可以阅读下面文章:

自定义属性创建皮肤

上一节中有说过,在Sass中可以通过变量来维护不同主题的样式代码。随着CSS自定义属性的出现(目前得到众多主流浏览器支持),我们可以使用CSS自定义属性来维护多套皮肤效果的CSS。

有关于CSS自定义属性的使用在这里就不做过多的阐述了。我们简单的看一个示例,CSS自定义属性如何实现皮肤切换,比如下面这个示例:

上面这个Demo来自于@Michelle Barker 的《How to create better themes with CSS variables》一文。

实现上例的效果,关键代码非常的少:

label,
main {
    background: var(--bg, white);
    color: var(--text, black);
}

main {
    --gradDark: hsl(144, 100%, 89%);
    --gradLight: hsl(42, 94%, 76%);
    background: linear-gradient(to bottom, var(--gradDark), var(--gradLight));
}

.theme-switch__input:checked ~ main,
.theme-switch__input:checked ~ label {
    --text: white;
}

.theme-switch__input:checked ~ main {
    --gradDark: hsl(198, 44%, 11%);
    --gradLight: hsl(198, 39%, 29%);
}

上面只是一个简单的小示例,其实使用CSS自定义属性还可以实现更为复杂的主题切换的效果,比如下面这个示例:

如果使用CSS自定义属性来维护黑暗模式和高亮模式的代码就会变得轻松地多:

:root {
    --primary-color: #302AE6;
    --secondary-color: #536390;
    --font-color: #424242;
    --bg-color: #fff;
    --heading-color: #292922;
}

[data-theme="dark"] {
    --primary-color: #9A97F3;
    --secondary-color: #818cab;
    --font-color: #e1e1ff;
    --bg-color: #161625;
    --heading-color: #818cab;
}

具体的案例可以看@Sebastiano Guerriero写的案例

在这个效果中,@Sebastiano Guerriero把Sass的变量和CSS的自定义属性结合在一起来维护黑暗模式和高亮模式的主题切换。在他的另一篇教程中,还把Local Storage API运用进来,感兴趣的可以点击这里了解

如果你对CSS自定义属性维护主题皮肤相关的话题感兴趣的话,可以花点时间阅读下面几篇文章:

纯CSS实现黑暗模式和高亮模式的切换

前面提到的几种方式或多或少都依赖一些JavaScript脚本,实际上借助于CSS的:target选择器,inputchecked可以让我们脱离JavaScript脚本,实现纯CSS完成黑暗模式和高亮模式的切换。

Web技巧系列的第八期中就有聊过:target:checked能替代JavaScript做哪些事情。

接下来看一个简单的示例,看看如何通过:checked来完成我们想要的效果。

使用:checked实现切换,对于HTML的结构有较强的要求,像下面这样来设计你的HTML结构基本上是不会有问题:

<input id="toggle" class="toggle sr-only" type="checkbox">

<div class="theme-container">
    <label class="toggle-label" for="toggle"></label>

    <div class="container">
        <!-- 页面的内容放置在这里 -->
    </div>
</div>

采用了type="checkbox"input,为了点击label可以让checkbox在选中和未选中之间切换,需要给input设置id名称,并且给label设置for属性值,最重要的是inputidlabelfor值相同。另外一点,div.theme-container必须在input标签的后面。对于自定义checkbox的样式怎么实现,这里就不做过多阐述。我们把重点放在黑暗模式和高亮模式切换的相关代码上。

我们将使用CSS自定义属性来做这些事情,首先在:root中定义的属性是高亮模式下相关的颜色:

:root {
    /* Light theme */
    --c-text: #333;
    --c-background: #fff;
}

因为默认情况之下是高亮模式,所以在.theme-container下的样式:

.theme-container {
    color: var(--c-text);
    background-color: var(--c-background);
}

checkbox:checked状态下将.theme-container中的自定义属性修改变黑暗模式的值:

input[type="checkbox"]:checked ~ .theme-container {
    /* Dark theme */
    --c-text: #fff;
    --c-background: #333;
}

详细代码可以在Codepen上查阅:

上面的效果在用户重新刷新时,会回到默认的高亮模式。如果你想让用户有一个更佳的体验,我们可以使用几行JavaScript代码来实现。

document.addEventListener('DOMContentLoaded', function () {
    const checkbox = document.querySelector('.toggle');

    checkbox.checked = localStorage.getItem('darkMode') === 'true';

    checkbox.addEventListener('change', function (event) {
        localStorage.setItem('darkMode', event.currentTarget.checked);
    });
});

下面的示例,可以自己体验一把:

这个案例再次验证了CSS能替代JavaScript做的一些事情。更多有关于交互行为可以直接使用CSS来实现,而不再依赖JavaScript的案例可以点击这里查阅

额外提一句,如果你觉得上述这样的方案强奸了你的HTML结构,你无法忍受,那么要实现类似的效果可以查看@ananyaneogi写的一个案例

纯系统级别(原生的)黑暗模式和高亮模式的切换

上面看到的所有案例,都不是原生的(系统级别)的。所谓的原生级别是切换系统的时候,对应显示相应的模式,即通过操作系统与浏览器通信完成。幸运地是,我们现在在部分的浏览器中可以直接使用纯CSS来完成操作系统通信。该CSS属性就是最新的媒体查询条件prefers-color-scheme,该特性允许我们检测用户是否通过媒体查询启用了什么模式:

@media (prefers-color-scheme: dark | light) {
    
}

该特性在Firefox 67+、Chrome 76+ 和 Safari 12.1+以及iOS Safari 13+都支持该特性:

如果你的浏览器已经支持了该特性,那么就可以直接像下面这样来完成原生的黑暗模式和高亮模式的切换:

:root {
    /* Light theme */
    --c-text: #333;
    --c-background: #fff;
}

.theme-container {
    color: var(--c-text);
    background-color: var(--c-background);
}

@media (prefers-color-scheme: dark) {
    :root {
        /* Dark theme */
        --c-text: #fff;
        --c-background: #333;
    } 
}

那么上面的示例,可以按上面的代码,甚至是结构变得更变简单:

如果你使用Firefox,可以在地址栏中输入about:config,然后鼠标右键点击选择“新建(New)” → “整数(Integer)”,新建整数ui.systemUsesDarkTheme,并且将其值设置为1

看到的效果如下:

如果你使用的是Safari浏览器,可以使用它自带的工具来查看效果:

最终在支持的浏览器下你将看到的效果如下:

在未来的Safari中,将会提供color-scheme属性:

:root {
    color-scheme: light dark;
}

:root上指定了lightdark两个值,让引擎知道文档支持这两种模式。这将更改页面的默认文本和背景颜色,以匹配当前系统外观。此外,标准表单控件、滚动条和其他命名的系统颜色会自动更改它们的外观。如果没有这个声明,引擎使用深色表单控件或深色配色方案将是不安全的,因为许多文档都是使用假定的浅色配色方案设计的。

例如,在页面中指定了color-scheme: light dark,将完全可以在两种外观中切换:

神奇的滤镜或混合模式来助你完成黑暗模式与高亮模式的切换

在Web技巧的第6期中,我们一起领略了CSS的filter和CSS的混合模式mix-blend-mode的神奇之处。

@thoughtspile在他的教程中向大家演示了如何使用filter实现黑暗模式:

.theme-dark {
    filter: invert(100) hue-rotate(180deg);
}

.theme-dark img {
    filter: invert(100) hue-rotate(180deg);
}

如果使用到了background-image的颜色,需要单独像处理img一样处理。

另外@wgao19在博文中介绍了如何使用mix-blend-mode来实现黑暗模式和高亮模式的切换:

.dark-mode-screen { 
    width: 100vw; 
    height: 100vh; 
    position: fixed; 
    top: 0; 
    left: 0; 
    background: white; 
    mix-blend-mode: difference; 
}

如果你想让页面中部分元素不忽略mix-blend-mode:difference带来的影响,可以使用isolation: isolate

.twitter-logo,
.emoji {
    isolation: isolate; 
}

效果会类似下图这样:

小结

上面我们通过不同的方式向大家阐述和演示了如何实现黑暗模式和高亮模式切换的解决方案。有粗暴简单的方式,原始的切换样式表的方式,还有采用一些新的CSS特性,比如CSS自定义属性,新的媒体查询特性,还有神奇的滤镜和混合模式。而且这些解决方案中既有CSS和JavaScript的混合解决方案,也有纯CSS的解决方案,甚至还有原生系统和浏览器通信的解决方案。还是那句老话,不管哪种解决方案或者技术手段,都有自己的利弊,没有最好,只有最适合的使用场景。在实际使用的时候,应该具体问题具体分析。

扩展阅读

如果你想更深入了解这方面的知识,建议你花一些时间阅读下面相关的教程: