深色模式适配和主题切换
作者:互联网
1.1 前置
如果你已经了解 CSS 自定义属性和匹配系统主题设置的相关知识,略过此部分。
1.1.1 CSS 自定义属性
“自定义属性”(有时候也被称作“CSS变量”或者“级联变量”)是由CSS作者定义的。声明变量时,变量名前要加上 --
,例如 --example: 20px
即是一个 css 自定义属性的声明语句。意思是将 20px 赋值给自定义变量 --example
。
在 css 的任何选择器中都可以声明 CSS 自定义属性,通常将所有 CSS 自定义属性声明在 :root
选择器中,以便在在整个文档中重复使用。:root
选择器匹配文档树的根元素。对于 HTML 文档来说,:root
匹配 <html>
元素,除了优先级更高之外,与 html 标签选择器相同。
示例:
:root {
--example: 20px
}
等价于:
html {
--example: 20px
}
通过 CSS 的 var()
函数读取自定义属性。例如:var(--example)
会返回 --example
所对应的值。var()
函数还可以使用第二个参数,表示自定义属性备用值。var()
会从左向右读取值,如果第一个变量不存在,就读取第二个。例如:var(--example, 40px)
, 如果 --example
不存在,将返回 40px。当然第二个参数同样可以使用 css 自定义属性而不是具体的值,例如:var(--example1, --example2)
。
示例:
<div class="container">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
.container div:nth-child(even) {
background-color: #90ee90;
}
.container div:nth-child(odd) {
background-color: #ffb6c1;
}
接下来,使用 css 自定义属性
+ :root {
+ --green: #90ee90;
+ --pink: #ffb6c1;
+ }
.container div:nth-child(even) {
- background-color: #90ee90;
+ background-color: var(--green);
}
.container div:nth-child(odd) {
- background-color: #ffb6c1;
+ background-color: var(--pink);
}
在上面的代码片段中,使用 CSS 自定义属性替换原来的颜色值,效果依然相同。
如果不考虑兼容 IE 浏览器,可以使用它,已经有大量的网站使用 CSS 自定义属性。要兼容 IE 也有办法,postcss-css-variables 插件将 CSS 自定义属性 (CSS 变量) 语法转换为静态表示形式。
1.1.2 跟随系统设置
使用 CSS 媒体查询匹配系统设置。prefers-color-scheme
用于检测用户是否有将系统的主题色设置为亮色或者暗色。
// 用户选择选择使用浅色主题的系统界面
@media (prefers-color-scheme: light) { }
// 用户选择选择使用深色主题的系统界面
@media (prefers-color-scheme: dark) { }
// 表示系统未得知用户在这方面的选项
@media (prefers-color-scheme: no-preference) { }
使 JavaScript matchedMedia API 匹配系统设置。
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
if (prefersDarkScheme.matches) {
// 用户系统主题设置为 dark
}
1.2 深色、浅色模式的实现
有多种方式实现深色模式。
1.2.1 使用 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
}
}
1.2.2 给 HTML 标签添加属性
:root
选择器会匹配 html 元素,给 <html>
动态添加 theme 属性,像这样 <html theme="dark">
。在 CSS 中使用属性选择器 :root[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" ,就能切换到该主题。
1.2.3 使用 class 和 CSS 自定义属性
类似的思路我们可以给 <html>
添加一个 class 来实现。
:root{
--color: #222;
}
:root.dark{
--color: #eee;
}
const button = document.querySelector('.toggle');
button.addEventListener('click', function() {
document.html.classList.toggle('dark');
})
1.2.4 仅使用 class
如果你的项目需要兼容 IE,仅使用 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;
}
试想以下,用户设置深色模式的操作系统并不意味着他们希望将深色模式应用到网站上。如果有此需求,可以先使用媒体查询覆盖深色模式。
:root {
--color: #000000;
}
:root.dark{
--color: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--color: #ffffff;
}
:root.light {
--color: #000000;
}
}
1.2.5 使用单独的 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";
}
});
1.2.6 Darkmode.js
GitHub 开源项目 Darkmode.js,通过 CSS 属性 mix-blend-mode
暴力实现深色模式,现在它有 2.2k Star。mix-blend-mode
描述当前元素的内容应该与当前元素的直系父元素的内容和元素的背景如何混合,值为 difference 时即“反相”。
尝试写个例子:
<body>
<div class="container">
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Delectus facere
rerum quasi nesciunt nam, nisi velit minima rem quaerat laboriosam natus
ab illum tempore atque repellendus tempora, vitae ratione repellat.
</p>
</div>
<div class="mix-mask"></div>
</body>
body {
background-color: #fff;
}
.container {
width: 600px;
margin: 60px auto 0;
padding: 40px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
}
.mix-mask {
display: none;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
mix-blend-mode: difference;
background-color: #fff;
}
使 .mix-mask 显示
.mix-mask {
- display: none;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
mix-blend-mode: difference;
background-color: #fff;
}
官网的示例:
它的源码十分简单,感兴趣可以了解下
// es module
// 通过 typeof 判断当前是否为浏览器环境,并导出常量
export const IS_BROWSER = typeof window !== "undefined";
// es6 支持导出 class
// class 只是一个语法糖,babel 转化
export default class Darkmode {
// constructor -> class实例化时执行
// 用户通过实例化该类并传递一个 options
// 构造函数接收 options -> 用户配置
constructor(options) {
if (!IS_BROWSER) {
return;
}
// 默认配置
const defaultOptions = {
bottom: "32px", // 按钮位置
right: "32px", // 按钮位置
left: "unset", // 按钮位置
time: "0.3s", // 过渡时间
mixColor: "#fff", // 混合层背景色
backgroundColor: "#fff", // 创建的背景层背景色
buttonColorDark: "#100f2c", // 亮色状态下的按钮颜色
buttonColorLight: "#fff", // 暗色状态下的按钮色
label: "", // 按钮中的内容
saveInCookies: true, // 是否存在cookie 默认 local storage
autoMatchOsTheme: true, // 跟随系统设置
};
// 通过 Object.assign 合并默认配置和用户配置
// 浅拷贝
options = Object.assign({}, defaultOptions, options);
// 需要在 css 使用配置
// style 以字符串的形式呈现
// 如果单独抽离css,需要更多的逻辑代码
const css = `
.darkmode-layer {
position: fixed;
pointer-events: none;
background: ${options.mixColor};
transition: all ${options.time} ease;
mix-blend-mode: difference;
}
.darkmode-layer--button {
width: 2.9rem;
height: 2.9rem;
border-radius: 50%;
right: ${options.right};
bottom: ${options.bottom};
left: ${options.left};
}
.darkmode-layer--simple {
width: 100%;
height: 100%;
top: 0;
left: 0;
transform: scale(1) !important;
}
.darkmode-layer--expanded {
transform: scale(100);
border-radius: 0;
}
.darkmode-layer--no-transition {
transition: none;
}
.darkmode-toggle {
background: ${options.buttonColorDark};
width: 3rem;
height: 3rem;
position: fixed;
border-radius: 50%;
border:none;
right: ${options.right};
bottom: ${options.bottom};
left: ${options.left};
cursor: pointer;
transition: all 0.5s ease;
display: flex;
justify-content: center;
align-items: center;
}
.darkmode-toggle--white {
background: ${options.buttonColorLight};
}
.darkmode-toggle--inactive {
display: none;
}
.darkmode-background {
background: ${options.backgroundColor};
position: fixed;
pointer-events: none;
z-index: -10;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
img, .darkmode-ignore {
isolation: isolate;
display: inline-block;
}
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
.darkmode-toggle {display: none !important}
}
@supports (-ms-ime-align:auto), (-ms-accelerator:true) {
.darkmode-toggle {display: none !important}
}
`;
// 混合层 -> 反相
const layer = document.createElement("div");
// 按钮 -> 点击切换夜间模式
const button = document.createElement("button");
// 背景层 -> 用户自定义背景色
const background = document.createElement("div");
// 初始化类(初始样式)
button.innerHTML = options.label;
button.classList.add("darkmode-toggle--inactive");
layer.classList.add("darkmode-layer");
background.classList.add("darkmode-background");
// 通过 localStorage 储存状态
// darkmodeActivated 获取当前是否在darkmode下
const darkmodeActivated =
window.localStorage.getItem("darkmode") === "true";
// 系统是否默认开启暗色模式
// matchMedia 方法的值可以是任何一个 CSS @media 规则 的特性。
// matchMedia 返回一个新的 MediaQueryList 对象,表示指定的媒体查询字符串解析后的结果。
// matches boolean 如果当前document匹配该媒体查询列表则其值为true;反之其值为false。
const preferedThemeOs =
options.autoMatchOsTheme &&
window.matchMedia("(prefers-color-scheme: dark)").matches;
// 是否储存localStorage
const darkmodeNeverActivatedByAction =
window.localStorage.getItem("darkmode") === null;
if (
(darkmodeActivated === true && options.saveInCookies) ||
(darkmodeNeverActivatedByAction && preferedThemeOs)
) {
// 激活夜间模式
layer.classList.add(
"darkmode-layer--expanded",
"darkmode-layer--simple",
"darkmode-layer--no-transition"
);
button.classList.add("darkmode-toggle--white");
// 激活 darkmode 时,将类 darkmode--activated 添加到body
document.body.classList.add("darkmode--activated");
}
// 插入
document.body.insertBefore(button, document.body.firstChild);
document.body.insertBefore(layer, document.body.firstChild);
document.body.insertBefore(background, document.body.firstChild);
// 将 css 插入 <style/>
this.addStyle(css);
// 初始化变量 button layer saveInCookies time
// 方便函数中调用
this.button = button;
this.layer = layer;
this.saveInCookies = options.saveInCookies;
this.time = options.time;
}
// 接收样式 css 字符串
// 创建 link 标签在 head 中插入
addStyle(css) {
const linkElement = document.createElement("link");
linkElement.setAttribute("rel", "stylesheet");
linkElement.setAttribute("type", "text/css");
// 使用encodeURIComponent将字符串编码
linkElement.setAttribute(
"href",
"data:text/css;charset=UTF-8," + encodeURIComponent(css)
);
document.head.appendChild(linkElement);
}
// 切换按钮
showWidget() {
if (!IS_BROWSER) {
return;
}
const button = this.button;
const layer = this.layer;
// s -> ms
const time = parseFloat(this.time) * 1000;
button.classList.add("darkmode-toggle");
button.classList.remove("darkmode-toggle--inactive");
layer.classList.add("darkmode-layer--button");
// 监听点击事件
button.addEventListener("click", () => {
// 当前是否在暗色模式
// isActivated()返回 bool 见下方
const isDarkmode = this.isActivated();
if (!isDarkmode) {
// 添加过渡样式
layer.classList.add("darkmode-layer--expanded");
// 禁用按钮
button.setAttribute("disabled", true);
setTimeout(() => {
// 清除过渡动画
layer.classList.add("darkmode-layer--no-transition");
// 显示混合层
layer.classList.add("darkmode-layer--simple");
// 取消禁用
button.removeAttribute("disabled");
}, time);
} else {
// 逻辑相反
layer.classList.remove("darkmode-layer--simple");
button.setAttribute("disabled", true);
setTimeout(() => {
layer.classList.remove("darkmode-layer--no-transition");
layer.classList.remove("darkmode-layer--expanded");
button.removeAttribute("disabled");
}, 1);
}
// 处理按钮样式,黑暗模式下背景色为白色调,反之为暗色调
// 如果 darkmode-toggle--white 类值已存在,则移除它,否则添加它
button.classList.toggle("darkmode-toggle--white");
// 如果 darkmode--activated 类值已存在,则移除它,否则添加它
document.body.classList.toggle("darkmode--activated");
// 取反存 localStorage
window.localStorage.setItem("darkmode", !isDarkmode);
});
}
// 允许使用方法 toggle()启用/禁用暗模式
// 即以编程的方式切换模式,而不是使用内置的按钮
// new Darkmode().toggle()
toggle() {
if (!IS_BROWSER) {
return;
}
const layer = this.layer;
const isDarkmode = this.isActivated();
// 处理样式
layer.classList.toggle("darkmode-layer--simple");
document.body.classList.toggle("darkmode--activated");
// 存状态
window.localStorage.setItem("darkmode", !isDarkmode);
}
// 检查是否激活了暗色模式
isActivated() {
if (!IS_BROWSER) {
return null;
}
// 通过判断body是否包含激活css class
// contains 数组方法 返回 bool
return document.body.classList.contains("darkmode--activated");
}
}
亮色模式状态下:
- 按钮:右下角黑色小方块,效果图中就是点击切换它切换暗色\亮色模式。
- 页面内容:图中蓝色部分。即该实例中的文本所在的层,包含其父级容器。
- 混合层:按钮下方小块。混合层亮色模式下不可见,通过上面的效果图你能明白该层在切换到夜间时经过过渡动画覆盖整个页面,除了 button。
- 自定义背景层:图中绿色边框所在层。用户自定义背景色,插件创建的层。
深色模式状态下:
与浅色模式状态对比,明显之处就是藏在按钮下方的小方块展开了,覆盖了整个页面。这个展开的小方块这就是混合层,这个层包含 CSS 属性 mix-blend-mode: difference
。正是如此实现的暗色模式。通过简单的“反相”,很显然并不能完美地实现深色模式,当网站内容较简单时或许可以尝试。
npm install --save darkmode-js
const options = {
// ...options
label: '标签:layer,深色,自定义,--,适配,dark,color,切换,darkmode
来源: https://www.cnblogs.com/guangzan/p/14723413.html