为 class 和媒体查询定义深色模式,无需重复 CSS 自定义属性声明,并允许用户在颜色模式之间切换
Define dark mode for both a class and a media query, without repeat CSS custom properties declarations, and allow users to switch between color modes
我有:
body { background: white; }
为了显示深色模式,我使用 .dark
class:
.dark body { background: black; }
为了检测用户是否将 OS 设置为使用深色主题,我们有 prefers-color-scheme:
@media (prefers-color-scheme: dark) {
body { background: black; }
}
然后我们有了 DRY(不要重复自己)编程的想法。我们能不能定义深色模式而不重复CSS属性声明,并且在这个过程中,允许用户通过JS在颜色模式之间切换?
上面的例子,.dark
class和媒体查询是互为副本的
到目前为止我做了什么
在 CSS 中跳过 prefers-color-scheme
并使用:
body { background: white; }
.dark body { background: black; }
然后通过JS检测他们的设置并调整
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.getElementsByTagName('html')[0].classList.add('dark');
}
这种方法的问题是它没有在 CSS 中使用 prefers-color-scheme
。
虽然我可以添加:
@media (prefers-color-scheme: dark) {
body { background: black; }
}
它不会让我通过 JS 切换 配色方案,因为我无法取消 prefers-color-scheme: dark
为 [=57] 设置了深色的用户=] 偏好。
2022 年解决这个问题的方法是什么?
您可以利用 CSS 媒体查询,而无需使用 JS 在 CSS 本身中实际使用它。
例如,从您的代码中提取 .dark
class,您可以使用 MediaQueryList
对象:
const darkMode = window.matchMedia("(prefers-color-scheme: dark)");
并为文档的 class 列表设置一个切换:
// Initial setting
document.documentElement.classList.toggle("dark", darkMode.matches);
// Listener to changes
darkMode.addListener((event) =>
document.documentElement.classList.toggle("dark", event.matches));
通过这种方式,您可以在 CSS 文件中专注于定义 .dark
class 下的 classes,而无需定义它两次使用媒体查询。此外,以这种方式,您可以为用户设置设置,以备不时之需覆盖默认的 OS 首选项(例如将其保存在 localStore
中,等等...)
如果你想用css
来做这个,你可以试试下面的方法
:root {
--body-background-color: white;
}
@media (prefers-color-scheme: dark) {
:root {
--body-background-color: black;
}
}
body {
background-color: var(--body-background-color);
}
你可以通过反转逻辑来实现。
body { background: black; }
@media (prefers-color-scheme: light) {
:root:not(.dark) body { background: white; }
}
下面是使用 CSS Variables、prefers-color-scheme
媒体查询和单选按钮的简单解决方案。在这里,页面检测 OS dark/light 主题,并允许用户通过单击没有 JavaScript 的单选按钮来更改主题。如果您确实需要 JavaScript.
,您还可以在这些单选按钮上触发点击事件
/* Defines theme variables */
:root {
--theme-light-color: #222222;
--theme-light-background: #FFFFFF;
--theme-dark-color: #DDDDDD;
--theme-dark-background: #222222;
}
/* Defines body content styles */
body {
margin: 0;
}
body>main {
min-height: 100vh;
color: var(--theme-main-color);
background: var(--theme-main-background);
}
label[for="theme-light"] {
color: var(--theme-light-color);
background: var(--theme-light-background);
cursor: pointer;
}
label[for="theme-dark"] {
color: var(--theme-dark-color);
background: var(--theme-dark-background);
cursor: pointer;
}
/* IF theme = light THEN */
#theme-light:checked~main {
--theme-main-color: var(--theme-light-color);
--theme-main-background: var(--theme-light-background);
}
@media (prefers-color-scheme: light){
:root {
--theme-main-color: var(--theme-light-color);
--theme-main-background: var(--theme-light-background);
}
}
/* IF theme = dark THEN */
#theme-dark:checked~main {
--theme-main-color: var(--theme-dark-color);
--theme-main-background: var(--theme-dark-background);
}
@media (prefers-color-scheme: dark){
:root {
--theme-main-color: var(--theme-dark-color);
--theme-main-background: var(--theme-dark-background);
}
}
<!DOCTYPE html>
<html lang="en">
<body>
<!-- THEME SWITCH -->
<input type="radio" id="theme-light" name="theme" hidden>
<input type="radio" id="theme-dark" name="theme" hidden>
<!-- BODY CONTENT -->
<main>
<label for="theme-light">Light</label>
<label for="theme-dark">Dark</label>
<p>Hello World!</p>
</main>
</body>
</html>
我们来分析一下。
我们绝对不能将暗模式样式 仅 放在媒体查询中,因为无法通过 in-site 选择应用它。所以我们希望样式在某个 class (dark
) 下没有媒体查询(这样 class 可以通过 JS 切换),但是媒体查询应该以某种方式拉即使没有 class,也没有重复样式。如果用户选择,我们可能还希望 light
class 覆盖媒体查询。
这里的完美解决方案是废弃的 @apply at-rule,它可以允许在单个变量下重用一堆样式。但这被放弃了。
重用一堆样式的标准建议是在 CSS 预处理器上使用混入(因此它不会在项目的源代码中重复,但在编译后的 CSS 中重复)。如果所有属性都应用于 <body>
元素或一些元素,那么我相信这是可行的方法。但是你要求一个没有预处理器的解决方案。
另一种方法是在 JS 中而不是在 CSS 本身中使用媒体查询,并相应地切换 class。这种方法甚至可以处理具有应用于许多元素的属性的复杂主题,但在 JS 生效之前它可能有一个 "Flash of unstyled content"。无论如何,您已经提出了这个问题,并寻求一种将媒体查询保留在 CSS 本身中的解决方案。
您还要求不要使用重复的 CSS 变量。现在,我不确定如果不重复 anything 是否可行,但是我们可以将重复减少到两个“切换”变量,而不管受影响的属性的数量如何。
var root = document.documentElement,
theme = window.getComputedStyle(root)
.getPropertyValue('--light') === ' ' ? 'dark' : 'light';
document.getElementById('toggle-theme')
.addEventListener('click', toggleTheme);
function toggleTheme() {
root.classList.remove(theme);
theme = (theme === 'dark') ? 'light' : 'dark';
root.classList.add(theme);
}
/*
"initial"ized variables are like undefined variables and will resolve to
their fallback value.
Whitespaced variables (the space is required) will serve as an empty value
in chaining.
*/
@media (prefers-color-scheme: dark) {
:root {
--light: ;
--dark: initial;
}
}
@media (prefers-color-scheme: light) {
:root {
--dark: ;
--light: initial;
}
}
:root.dark {
--light: ;
--dark: initial;
}
:root.light {
--dark: ;
--light: initial;
}
#content {
background-color: var(--dark, darkblue) var(--light, lightblue);
color: var(--dark, white) var(--light, black);
border: 5px solid var(--dark, blue) var(--light, yellow);
}
<div id="content">
Hello, world!
<button id="toggle-theme">Toggle theme</button>
</div>
最后,我想到了另一种可能的破解方法,尽管我真的不喜欢它。这个想法是添加两个 <div>
,其唯一目的是基于 preferred/selected 主题继承样式。
var root = document.documentElement,
theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.getElementById('toggle-theme')
.addEventListener('click', toggleTheme);
function toggleTheme() {
root.classList.remove(theme);
theme = (theme === 'dark') ? 'light' : 'dark';
root.classList.add(theme);
}
.dark-styles {
background: darkblue;
color: white;
border-color: blue;
}
.light-styles {
background: lightblue;
color: black;
border-color: yellow;
}
:root.dark .light-styles {
all: inherit;
}
@media (prefers-color-scheme:dark) {
:root:not(.light) .light-styles {
all: inherit;
}
}
#content {
border: 5px solid;
border-color: inherit;
}
<div class="dark-styles">
<div class="light-styles">
<div id="content">
Hello, world!
<button id="toggle-theme">Toggle theme</button>
</div>
</div>
</div>
我有:
body { background: white; }
为了显示深色模式,我使用 .dark
class:
.dark body { background: black; }
为了检测用户是否将 OS 设置为使用深色主题,我们有 prefers-color-scheme:
@media (prefers-color-scheme: dark) {
body { background: black; }
}
然后我们有了 DRY(不要重复自己)编程的想法。我们能不能定义深色模式而不重复CSS属性声明,并且在这个过程中,允许用户通过JS在颜色模式之间切换?
上面的例子,.dark
class和媒体查询是互为副本的
到目前为止我做了什么
在 CSS 中跳过 prefers-color-scheme
并使用:
body { background: white; }
.dark body { background: black; }
然后通过JS检测他们的设置并调整
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.getElementsByTagName('html')[0].classList.add('dark');
}
这种方法的问题是它没有在 CSS 中使用 prefers-color-scheme
。
虽然我可以添加:
@media (prefers-color-scheme: dark) {
body { background: black; }
}
它不会让我通过 JS 切换 配色方案,因为我无法取消 prefers-color-scheme: dark
为 [=57] 设置了深色的用户=] 偏好。
2022 年解决这个问题的方法是什么?
您可以利用 CSS 媒体查询,而无需使用 JS 在 CSS 本身中实际使用它。
例如,从您的代码中提取 .dark
class,您可以使用 MediaQueryList
对象:
const darkMode = window.matchMedia("(prefers-color-scheme: dark)");
并为文档的 class 列表设置一个切换:
// Initial setting
document.documentElement.classList.toggle("dark", darkMode.matches);
// Listener to changes
darkMode.addListener((event) =>
document.documentElement.classList.toggle("dark", event.matches));
通过这种方式,您可以在 CSS 文件中专注于定义 .dark
class 下的 classes,而无需定义它两次使用媒体查询。此外,以这种方式,您可以为用户设置设置,以备不时之需覆盖默认的 OS 首选项(例如将其保存在 localStore
中,等等...)
如果你想用css
来做这个,你可以试试下面的方法
:root {
--body-background-color: white;
}
@media (prefers-color-scheme: dark) {
:root {
--body-background-color: black;
}
}
body {
background-color: var(--body-background-color);
}
你可以通过反转逻辑来实现。
body { background: black; }
@media (prefers-color-scheme: light) {
:root:not(.dark) body { background: white; }
}
下面是使用 CSS Variables、prefers-color-scheme
媒体查询和单选按钮的简单解决方案。在这里,页面检测 OS dark/light 主题,并允许用户通过单击没有 JavaScript 的单选按钮来更改主题。如果您确实需要 JavaScript.
/* Defines theme variables */
:root {
--theme-light-color: #222222;
--theme-light-background: #FFFFFF;
--theme-dark-color: #DDDDDD;
--theme-dark-background: #222222;
}
/* Defines body content styles */
body {
margin: 0;
}
body>main {
min-height: 100vh;
color: var(--theme-main-color);
background: var(--theme-main-background);
}
label[for="theme-light"] {
color: var(--theme-light-color);
background: var(--theme-light-background);
cursor: pointer;
}
label[for="theme-dark"] {
color: var(--theme-dark-color);
background: var(--theme-dark-background);
cursor: pointer;
}
/* IF theme = light THEN */
#theme-light:checked~main {
--theme-main-color: var(--theme-light-color);
--theme-main-background: var(--theme-light-background);
}
@media (prefers-color-scheme: light){
:root {
--theme-main-color: var(--theme-light-color);
--theme-main-background: var(--theme-light-background);
}
}
/* IF theme = dark THEN */
#theme-dark:checked~main {
--theme-main-color: var(--theme-dark-color);
--theme-main-background: var(--theme-dark-background);
}
@media (prefers-color-scheme: dark){
:root {
--theme-main-color: var(--theme-dark-color);
--theme-main-background: var(--theme-dark-background);
}
}
<!DOCTYPE html>
<html lang="en">
<body>
<!-- THEME SWITCH -->
<input type="radio" id="theme-light" name="theme" hidden>
<input type="radio" id="theme-dark" name="theme" hidden>
<!-- BODY CONTENT -->
<main>
<label for="theme-light">Light</label>
<label for="theme-dark">Dark</label>
<p>Hello World!</p>
</main>
</body>
</html>
我们来分析一下。
我们绝对不能将暗模式样式 仅 放在媒体查询中,因为无法通过 in-site 选择应用它。所以我们希望样式在某个 class (dark
) 下没有媒体查询(这样 class 可以通过 JS 切换),但是媒体查询应该以某种方式拉即使没有 class,也没有重复样式。如果用户选择,我们可能还希望 light
class 覆盖媒体查询。
这里的完美解决方案是废弃的 @apply at-rule,它可以允许在单个变量下重用一堆样式。但这被放弃了。
重用一堆样式的标准建议是在 CSS 预处理器上使用混入(因此它不会在项目的源代码中重复,但在编译后的 CSS 中重复)。如果所有属性都应用于 <body>
元素或一些元素,那么我相信这是可行的方法。但是你要求一个没有预处理器的解决方案。
另一种方法是在 JS 中而不是在 CSS 本身中使用媒体查询,并相应地切换 class。这种方法甚至可以处理具有应用于许多元素的属性的复杂主题,但在 JS 生效之前它可能有一个 "Flash of unstyled content"。无论如何,您已经提出了这个问题,并寻求一种将媒体查询保留在 CSS 本身中的解决方案。
您还要求不要使用重复的 CSS 变量。现在,我不确定如果不重复 anything 是否可行,但是我们可以将重复减少到两个“切换”变量,而不管受影响的属性的数量如何。
var root = document.documentElement,
theme = window.getComputedStyle(root)
.getPropertyValue('--light') === ' ' ? 'dark' : 'light';
document.getElementById('toggle-theme')
.addEventListener('click', toggleTheme);
function toggleTheme() {
root.classList.remove(theme);
theme = (theme === 'dark') ? 'light' : 'dark';
root.classList.add(theme);
}
/*
"initial"ized variables are like undefined variables and will resolve to
their fallback value.
Whitespaced variables (the space is required) will serve as an empty value
in chaining.
*/
@media (prefers-color-scheme: dark) {
:root {
--light: ;
--dark: initial;
}
}
@media (prefers-color-scheme: light) {
:root {
--dark: ;
--light: initial;
}
}
:root.dark {
--light: ;
--dark: initial;
}
:root.light {
--dark: ;
--light: initial;
}
#content {
background-color: var(--dark, darkblue) var(--light, lightblue);
color: var(--dark, white) var(--light, black);
border: 5px solid var(--dark, blue) var(--light, yellow);
}
<div id="content">
Hello, world!
<button id="toggle-theme">Toggle theme</button>
</div>
最后,我想到了另一种可能的破解方法,尽管我真的不喜欢它。这个想法是添加两个 <div>
,其唯一目的是基于 preferred/selected 主题继承样式。
var root = document.documentElement,
theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.getElementById('toggle-theme')
.addEventListener('click', toggleTheme);
function toggleTheme() {
root.classList.remove(theme);
theme = (theme === 'dark') ? 'light' : 'dark';
root.classList.add(theme);
}
.dark-styles {
background: darkblue;
color: white;
border-color: blue;
}
.light-styles {
background: lightblue;
color: black;
border-color: yellow;
}
:root.dark .light-styles {
all: inherit;
}
@media (prefers-color-scheme:dark) {
:root:not(.light) .light-styles {
all: inherit;
}
}
#content {
border: 5px solid;
border-color: inherit;
}
<div class="dark-styles">
<div class="light-styles">
<div id="content">
Hello, world!
<button id="toggle-theme">Toggle theme</button>
</div>
</div>
</div>