CSS 中的 DRY 颜色主题方法

Avatar of Christopher Kirk-Nielsen
Christopher Kirk-Nielsen

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 $200 免费积分!

前几天,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 了多少?

多个主题? 没问题!

你可能可以使用超过两种模式来实现这一点。 添加的主题越多,管理起来就越复杂,但是关键是,这是可能的! 我们添加另一组 ONOFF 变量的主题,并在值列表中设置一个额外的变量。

.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 布尔值的一个很好的用例吗? 好吧,这就是梦想。

你呢? 你有没有用其他方法解决这个问题? 在评论区分享你的想法吧!