前几天,Florens Verschelde 提问关于如何定义 暗黑模式样式,既能用于类,也能用于媒体查询,而无需重复 CSS 自定义属性声明。 我过去也遇到过这个问题,但一直没有找到合适的解决方案。
我们想要避免在切换明暗模式时重新定义(从而重复)自定义属性。 这是 DRY(不要重复自己)编程的目标,但是切换主题的典型模式通常是这样的
:root {
--background: #fff;
--text-color: #0f1031;
/* etc. */
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0f1031;
--text-color: #fff;
/* etc. */
}
}
明白我的意思了吗? 当然,在这个简化的例子中,它可能看起来并不重要,但是想象一下,如果要同时处理几十个自定义属性——那就是很多重复了!
然后我想起了 Lea Verou 的技巧,使用 --var: ;
,虽然一开始我并没有意识到,但我找到了一个让它起作用的方法:不是使用 var(--light-value, var(--dark-value))
或者类似的嵌套组合,而是将两者并排使用!
当然,肯定有人比我更早地发现了这个方法,但我还没有听说过利用(或者更确切地说,是滥用)CSS 自定义属性来实现这一点。 废话不多说,这就是这个想法
--color: var(--light, orchid) var(--dark, rebeccapurple);
如果 --light
值设置为 initial
,将使用回退值(orchid
),这意味着 --dark
应该设置为一个空白字符(这是一个有效值),使最终计算出的值看起来像这样
--color: orchid ; /* Note the additional whitespace */
反之,如果 --light
设置为一个空白字符,--dark
设置为 initial
,最终计算出的值将是
--color: rebeccapurple; /* Again, note the whitespace */
现在,这很棒,但我们需要根据上下文定义 --light
和 --dark
自定义属性。 用户可以拥有一个系统首选项(明暗模式),也可以通过某些 UI 元素切换网站的主题。 就像 Florens 的例子一样,我们将定义这三种情况,并进行一些微小的可读性增强,Lea 建议使用“on”和“off”常量,以便一目了然
:root {
/* Thanks Lea Verou! */
--ON: initial;
--OFF: ;
}
/* Light theme is on by default */
.theme-default,
.theme-light {
--light: var(--ON);
--dark: var(--OFF);
}
/* Dark theme is off by default */
.theme-dark {
--light: var(--OFF);
--dark: var(--ON);
}
/* If user prefers dark, then that's what they'll get */
@media (prefers-color-scheme: dark) {
.theme-default {
--light: var(--OFF);
--dark: var(--ON);
}
}
然后,我们可以在单个声明中设置所有主题变量,而不会重复。 在这个例子中,theme-*
类被设置为 html
元素,所以我们可以使用 :root
作为选择器,就像许多人喜欢做的那样,但是你也可以将它们设置为 body
,如果自定义属性的级联特性更适合这种方式
:root {
--text: var(--light, black) var(--dark, white);
--bg: var(--light, orchid) var(--dark, rebeccapurple);
}
要使用它们,我们使用 var()
,并带有内置回退,因为我们喜欢谨慎
body {
color: var(--text, navy);
background-color: var(--bg, lightgray);
}
希望你现在已经开始看到这里的好处了。 我们不再需要定义和切换一大堆自定义属性,而是处理两个,并在 :root
上只设置其他所有属性。 这与我们最初相比有了巨大的改进。
使用预处理器更 DRY
如果你要让我在没有上下文的情况下查看下面这一行代码,我肯定会感到困惑,因为颜色是一个单一值,而不是两个!
--text: var(--light, black) var(--dark, white);
这就是为什么我更喜欢将事情抽象化一点。 我们可以使用我们最喜欢的预处理器设置一个函数,在我的例子中是 Sass。 如果我们保留上面定义的 --light
和 --dark
值的代码,我们只需要在实际的自定义属性声明中进行修改。 让我们创建一个 light-dark
函数,它将为我们返回 CSS 语法
@function light-dark($light, $dark) {
@return var(--light, #{ $light }) var(--dark, #{ $dark });
}
我们可以这样使用它
:root {
--text: #{ light-dark(black, white) };
--bg: #{ light-dark(orchid, rebeccapurple) };
--accent: #{ light-dark(#6d386b, #b399cc) };
}
你会注意到函数调用周围有插值分隔符 #{ … }
。 如果没有这些,Sass 会按原样输出代码(就像一个普通 CSS 函数一样)。 你可以尝试各种实现方法,但语法的复杂程度取决于你的喜好。
代码库 DRY 了多少?
多个主题? 没问题!
你可能可以使用超过两种模式来实现这一点。 添加的主题越多,管理起来就越复杂,但是关键是,这是可能的! 我们添加另一组 ON
或 OFF
变量的主题,并在值列表中设置一个额外的变量。
.theme-pride {
--light: var(--OFF);
--dark: var(--OFF);
--pride: var(--ON);
}
:root {
--text:
var(--light, black)
var(--dark, white)
var(--pride, #ff8c00)
; /* Line breaks are absolutely valid */
/* Other variables to declare… */
}
这是个黑客方法吗? 是的,绝对是。 这是未来可能存在的、尚未出现的 CSS 布尔值的一个很好的用例吗? 好吧,这就是梦想。
你呢? 你有没有用其他方法解决这个问题? 在评论区分享你的想法吧!
很棒的黑客技巧! 我很喜欢。
想知道是否有 Sass 函数可以帮你做这件事? 看起来是可能的。 谢谢你的分享!
当然可以! 你可以使用预处理器来处理混乱的语法,请查看本文的 这一节。 或者使用一些主题标记进一步实现自动化:https://codepen.io/chriskirknielsen/pen/QWGaXqP
我觉得这有点像反模式。 如果你想添加额外的主题,你总是要修改基本定义并混合新的主题。 我更喜欢这种方法,其中主题只覆盖它需要覆盖的属性。 这样,如果你使用预处理器,就可以将每个主题放在单独的文件中。 添加或删除主题只需一次导入,而无需逐个修改所有属性。
这绝对是一种黑客方法,不是什么最佳实践。 它更像是“侏罗纪公园”科学家所想的那种方法,他们只考虑能做什么,而不是应该做什么。 ;)
如果你确实需要经常更改主题,并且确实想要使用这种方法,那么最好在预处理器中使用一个更复杂的函数来抽象化一层,该函数读取每个主题的标记映射。 查看这个演示,了解如何实现:https://codepen.io/chriskirknielsen/pen/QWGaXqP?editors=0100 我并不是说这是最好的方法,只是它可以实现。 :)
很有趣! 你会多长时间使用这种方法,或者其他类似的解决方案? 我是从一个更精通后端,拥有大量 React 经验的人的角度来问这个问题的——我可能会直接使用两组独立的样式表,并进行动态导入。 在一定程度上之后,这种方法是否无法持续使用?
我认为这是一个很酷的技巧,但说实话,我不能说它在生产环境中的应用——这更像是一个实验。
你当然可以有一个全局样式表,它读取你的
--text
、--bg
等,以及单独的样式表来声明这些变量,然后动态地交换它们。 只要在加载新的文件时保留旧的主题文件,就可以避免所有变量都丢失的页面。如果你已经使用 React 了,我认为这个技巧可能没有那么有用。 我对 React 也不是很精通,所以可能已经有一些很酷的技巧可以实现类似的效果,但这是一种 CSS 解决方案(如果你不考虑
theme-*
类切换的话),我认为它很有趣,可以谈谈。 :)除了现在你必须在每个声明中重复自己,而不是在文件开头重复两次
除非我误解了你的意思,否则情况并非如此:一旦你像
--text
或--accent
那样在:root
中声明了变量,它就可以在整个样式表中使用,无需重新声明。使用之前帖子中的动画帧技巧自定义属性(https://css-tricks.cn/css-switch-case-conditions/),我发现你可以让代码更 DRY
这是另一种方法; 任何你喜欢的方法都可以! 如果你的主题数量奇特或很多,那么在使用百分比时确实需要多花一点心思,但在大多数情况下,一个明暗模式就足够了,你的方法很有效!
我记得几年前用过类似的技巧来实现响应式字体大小,但需要使用 JS 根据视窗大小调整负延迟。 使用自定义属性,事情变得容易多了!
我意识到你并不需要使用整个动画,所以你可以将每个关键帧映射到 1%,并将持续时间设置为 100 秒,这足以满足大多数应用。 我对完全使用这种动画关键帧方法的主要担忧是,它是否会带来重大的性能成本或可访问性问题。 我对动画实现的内部机制了解得不够,无法判断这是否会造成巨大的内存成本,或者是否会受到用户设置的影响。
我几个月前写了这个。 https://dev.to/drno/theming-and-coloring-finally-made-efficient-in-css-thanks-to-an-oop-inspired-pattern-27ca 在生产环境中使用这种模式的更高级版本已经超过一年了。 想知道你对此有什么想法
PS:V2 几天后发布