适配深色模式

为博客适配深色模式。

<封面摄于江苏·南京的总统府,小潘同学就是在这被猫猫挠了。>

滤镜反色

最偷懒的方式就是用 CSS3 的滤镜对整个页面进行反色,只需一行代码。

html[theme='dark-mode'] {
filter: invert(1) hue-rotate(180deg);
}

使用CSS3滤镜进行反色

这时候会发现有个小问题,就是图片也会被反色,形成类似胶卷底片的效果。那么只需将图片再反色回来即可。

html[theme='dark-mode'] img{
filter: invert(1) hue-rotate(180deg);
}

适配图片

分分钟搞定,但是似乎非黑即白,还不够细腻。

一行代码使用CSS的黑暗模式

媒体查询和样式变量

媒体查询 (@media) 中的 prefers-color-scheme 用于检测用户是否有将系统的主题色设置为浅色或者深色,配合 CSS Variable 我们可以为浅色或深色模式单独匹配样式,实现更细腻的深色模式。

Media Query

prefers-color-scheme 有以下三个值:

  • light: 检测出系统处于 浅色 模式
  • dark: 检测出系统处于 深色 模式
  • no-preference: 并未检测出系统所处的颜色模式,可能是出于系统不支持或者被隐私保护拦截等因素

使用方法如下:

<div class="background">
<span class="text"></span>
</div>
@media (prefers-color-scheme: dark) {  /* 深色模式 */
.background { background: #333333; }
.text { color: #ffffff; }
}

@media (prefers-color-scheme: light) { /* 浅色模式 */
.background { background: #ffffff; }
.text { color: #333333; }
}

兼容性如下,不过对于不支持该属性的浏览器也能忽略该属性从而向下兼容。

浏览器兼容性

通过该属性,我们即可检测出系统当前所处的颜色模式,并对样式进行单独配置。

MDN: prefers-color-scheme

CSS Variable

通过 prefers-color-scheme 匹配颜色模式,但是为所有元素都单独定制两套颜色样式显然很麻烦,后期也难以维护。

一个页面内的颜色方案通常比较统一,也就那么几种颜色,所以我们可以通过 CSS变量 (CSS Variable) 为颜色进行规整,快速切换颜色模式。

:root {
--color-background: #ffffff;
--color-text: #33333d;
}

@media (prefers-color-scheme: dark) {
:root {
--color-background: #1e2128;
--color-text: #dddddd;
}
}

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

手动切换

通过媒体查询和样式变量我们可以跟随系统设置,实现自动切换颜色模式。但是这样还不够友好,有以下场景:

  1. 浏览器不支持 prefers-color-scheme,无法自动切换颜色模式
  2. 系统处于深色模式状态,但是我又想让该网页单独显示浅色模式

所以还需要添加一个按钮,让用户手动切换颜色模式。

HTML Attribute

实现用户手动切换颜色模式,首先需要一个“全局变量”来保存当前颜色模式,并且让 CSS 识别该“变量”,匹配颜色模式。我们可以直接在 html 标签(根元素)设定一个属性 color-mode,属性值有 lightdark,可以通过 CSS 的属性选择器直接匹配,用户点击切换按钮时可以通过 JS 直接修改该属性。

<html color-mode="dark">
<div class="background">
<span class="text"></span>
</div>
</html>
:root {
--color-background: #ffffff;
--color-text: #33333d;
}

[color-mode='dark'] {
:root {
--color-background: #1e2128;
--color-text: #dddddd;
}
}

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

Stylus

hexo-theme-zhaoo 主题使用了 Stylus 预处理器,基于 变量函数 等特性可以进一步抽离样式,便于维护。

/* variables.styl */

/* light */
$color-background = unquote(hexo-config('color.background') || #ffffff)
$color-background-secondary = unquote(hexo-config('color.background-secondary') || #f6f8fa)
$color-background-rgb = 255, 255, 255
$color-text = unquote(hexo-config('color.text') || #33333d)
$color-text-secondary = unquote(hexo-config('color.text-secondary') || #4e4e4e)
$color-text-third = unquote(hexo-config('color.text-third') || #999999)
/* dark */
$color-background-dark = unquote(hexo-config('color.background-dark') || #1e2128)
$color-background-secondary-dark = unquote(hexo-config('color.background-secondary-dark') || #1a1d22)
$color-background-rgb-dark = 30, 33, 40
$color-text-dark = unquote(hexo-config('color.text-dark') || #dddddd)
$color-text-secondary-dark = unquote(hexo-config('color.text-secondary-dark') || #9899ab)
$color-text-third-dark = unquote(hexo-config('color.text-third-dark') || #7d8594)
/* color-mode.styl */

:root
--color-background $color-background
--color-background-secondary $color-background-secondary
--color-background-rgb $color-background-rgb
--color-text $color-text
--color-text-secondary $color-text-secondary
--color-text-third $color-text-third
dark()
--color-background $color-background-dark
--color-background-secondary $color-background-secondary-dark
--color-background-rgb $color-background-rgb-dark
--color-text $color-text-dark
--color-text-secondary $color-text-secondary-dark
--color-text-third $color-text-third-dark
@media (prefers-color-scheme dark)
:root:not([color-mode])
dark()
[color-mode='dark']
dark()

触发器

触发器就是一个按钮,点击后修改 html 标签的 color-mode 属性,切换颜色模式。比较简单,直接上代码了:

<i class="iconfont iconmoono" id="color-toggle" color-toggle="light"></i>
var switchColorMode = function () {
if (!document.getElementById('color-toggle')) return;
document.getElementById('color-toggle').addEventListener('click', function () {
var mode = this.getAttribute('color-toggle') === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute(htmlAttribute, mode);
});
}
switchColorMode();

至此,用户就可以点击按钮手动切换颜色模式了。

缓存状态

该方案还存在问题,跳转页面或刷新页面,颜色模式就会切回到默认。(用户:你***逗我玩呢?)所以我们需要让浏览器缓存用户手动切换的颜色模式,之后加载页面时默认以该模式渲染。背面经环节:前端缓存方案有 cookieslocalStoragesessionStorageWeb SQLIndexedDB……

用最方便的 localStorage 储存用户切换到颜色模式即可,在用户点击按钮后将更新的颜色模式通过 localStorage.setItem 存储,再在页面渲染时通过 localStorage.getItem 获取颜色模式并渲染即可,keycolor-mode

我们还需要解决一个问题,系统自动配置 (媒体查询) 与 用户手动配置 (按钮切换) 之间的同步和冲突问题。例如:1. 在固定时段(晚上或白天),页面渲染时按用户切换的颜色模式加载。2. 在时段改变后(白天变为晚上),页面渲染时按系统颜色模式渲染。

我们只需要再添加一组 keycolor-mode-media-querylocalStorage,缓存媒体查询的颜色模式。渲染时判断 当前媒体查询缓存 是否相等,相等说明处于同一时段,不等说明时段已改变,从而决定渲染方式。

最后,这段 JS 需要添加到 </body> 标签前面加载,不然会闪屏。

完整 JS 代码如下:

!function (window, document) {
var rootElement = document.documentElement;
var toggleElement = document.getElementById('color-toggle');
var highlightElement = document.getElementsByName('highlight-style');
var modeStorageKey = 'color-mode';
var mediaQueryStorageKey = 'color-mode-media-query';
var htmlAttribute = 'color-mode';
var toggleAttribute = 'color-toggle';

var getMediaQuery = function () {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}

var getModeStorage = function () {
return localStorage.getItem(modeStorageKey);
}

var setModeStorage = function (mode) {
localStorage.setItem(modeStorageKey, mode);
}

var getMediaQueryStorage = function () {
return localStorage.getItem(mediaQueryStorageKey);
}

var setMediaQueryStorage = function (mode) {
localStorage.setItem(mediaQueryStorageKey, mode);
}

var setColorMode = function (mode) {
rootElement.setAttribute(htmlAttribute, mode);
setModeStorage(mode);
}

var setIcon = function (mode) {
if (!toggleElement) return;
var addIconName = mode === 'light' ? 'iconmoono' : 'iconsuno';
var removeIconName = mode === 'light' ? 'iconsuno' : 'iconmoono';
toggleElement.classList.remove(removeIconName);
toggleElement.classList.add(addIconName);
toggleElement.setAttribute(toggleAttribute, mode);
}

var setHighlightStyle = function (mode) {
highlightElement.forEach(function (item) {
item.disabled = !(item.getAttribute('mode') === mode);
});
}

var loadColorMode = function (mode) {
var mode = mode || getModeStorage() || getMediaQuery();
if (getMediaQuery() === getMediaQueryStorage()) {
mode = getModeStorage();
} else {
mode = getMediaQuery();
setMediaQueryStorage(mode);
}
setColorMode(mode);
setIcon(mode);
setHighlightStyle(mode);
}

var switchColorMode = function () {
if (!toggleElement) return;
toggleElement.addEventListener('click', function () {
var mode = this.getAttribute(toggleAttribute) === 'light' ? 'dark' : 'light';
setColorMode(mode);
setIcon(mode);
setHighlightStyle(mode);
});
}

loadColorMode();
switchColorMode();
}(window, document);

[你好黑暗,我的老朋友 —— 为网站添加用户友好的深色模式支持] (https://blog.skk.moe/post/hello-darkmode-my-old-friend/)

适配特殊样式

大部分元素可以通过 CSS 属性直接匹配样式,但是仍有一部分元素需要通过“特殊”方法进行处理。

PNG

适配首屏云朵,其实就是张 PNG 图片,要是 PNG 能用 CSS 控制颜色就好了。

适配PNG

从张鑫旭大佬的博客找到了解决方案,可以用 CSS3 滤镜中的投影 (filter: drop-shadow) 进行上色。但是需要做一些处理,将原图隐藏而阴影显示,其实只要将原图偏移出视口,再将阴影偏移回正确位置即可。

img {
position: absolute;
width: 100vw;
left: -100vw; /* 原图偏移出视口 */
filter: drop-shadow(var(--color-background) 100vw 0px); /* 阴影进行上色,并偏移回原位置 */
}

PNG格式小图标的CSS任意颜色赋色技术

SVG

接下来适配首屏波浪,小图标等 SVG 内容。

适配SVG

首先 SVG 也是可以用上面提到的 filter: drop-shadow 进行上色的。

另外,也可以用 SVG 标签中的 fill 属性进行赋色,如下:

<svg class="preview-waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
</defs>
<g class="preview-parallax">
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(var(--color-background-rgb), 0.7" /> //通过 fill 属性进行赋色
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(var(--color-background-rgb), 0.5)" /> //通过 fill 属性进行赋色
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(var(--color-background-rgb), 0.3)" /> //通过 fill 属性进行赋色
<use xlink:href="#gentle-wave" x="48" y="7" fill="rgb(var(--color-background-rgb))" /> //通过 fill 属性进行赋色
</g>
</svg>

rgba

对应含有透明通道的颜色 (rgba),如果在 rgba() 中包裹 CSS变量,stylus 会解析出错,这应该是 stylus 的一个 bug。我们可以用 stylus 中的 @css 指令解决,被包裹在 @css 中的内容将不会被 stylus 解析,而是直接以 CSS 的形式输出。如下:

@css {
.menu {
background-color: rgba(var(--color-background-rgb), 0.7);
}
.navbar {
background-color: rgba(var(--color-background-rgb), 0.8)s;
}
}

highlight

代码高亮也需要做适配,可以引入浅色和深色两套代码高亮样式,默认用 disabled 属性禁用,然后在页面渲染时根据颜色模式开启对应的代码高亮样式。如下:

<% if(theme.highlight.enable){ %>
<% if(theme.vendors.highlight_css){ %>
<% for (i in theme.highlight.style) { %>
<% style = theme.highlight.style[i].toLowerCase().replace(/(?<!([0-9]))\s(?!([0-9]))/g, '-').replace(/\s/g, '') %> //引入多套样式
<%- css({href: theme.vendors.highlight_css + style + '.min.css', name: 'highlight-style', mode: i}) %>
<% } %>
<% }else{ %>
<%- css('lib/highlight/a11y-dark.css')%>
<% }} %>
var setHighlightStyle = function (mode) {
highlightElement.forEach(function (item) {
item.disabled = !(item.getAttribute('mode') === mode);
});
}
查看评论