A few ways of specifying per-theme colours in only CSS

A few ways of specifying per-theme colours in only CSS

仅使用 CSS 指定主题颜色的几种方法

🗓️ 2026-05-16 • Tagged /css, /meta

I was thinking about this as part of putting this website together. Actually it was because I forgot about light-dark(); if I’d remembered that earlier I probably wouldn’t have ended up with all this! 在构建这个网站时,我思考过这个问题。其实是因为我忘记了 light-dark() 函数;如果我早点想起它,可能就不会折腾出这么多东西了!

My requirements: (which may not match your requirements) 我的需求如下:(可能与你的需求不完全一致)

  • Must support auto (based on prefers-color-scheme), light, and dark, chosen by radio buttons.
  • 必须支持自动(基于 prefers-color-scheme)、浅色和深色模式,并通过单选按钮进行选择。
  • Must work without any JavaScript (persistence is out of scope).
  • 必须在没有任何 JavaScript 的情况下工作(持久化存储不在讨论范围内)。

The assumed basic HTML and CSS

预设的基础 HTML 和 CSS

Up to you exactly how to include and structure it, but all the examples that follow assume something like this: 具体如何引入和构建结构由你决定,但后续的所有示例都假设采用类似这样的结构:

<fieldset>
  <legend>Theme</legend>
  <label><input type=radio name=theme id=theme-auto checked> Follow system</label>
  <label><input type=radio name=theme id=theme-light> Light</label>
  <label><input type=radio name=theme id=theme-dark> Dark</label>
</fieldset>

I’m going to assume the availability of a few CSS features: 我将假设以下几种 CSS 特性可用:

  • @media (prefers-color-scheme: dark) for automatic selection. It shipped in 2019–2020, which is generally enough.
  • 用于自动选择的 @media (prefers-color-scheme: dark)。它在 2019-2020 年间发布,通常已经足够普及。
  • :has() for manual selection without needing additional JavaScript. It’s newer, supported back to 2023-12; I consider that enough to rely on in general, but if you’re not happy with it, you have two options:
  • 用于手动选择且无需额外 JavaScript 的 :has()。它较新,支持追溯到 2023 年 12 月;我认为这已经足够可靠,但如果你不满意,有两个替代方案:
    • Only support one behaviour (probably auto) sans-JS. This is a very reasonable choice. You might add a light or dark class to the root element.
    • 仅支持一种无需 JS 的行为(通常是自动)。这是一个非常合理的选择。你可以向根元素添加 lightdark 类。
    • Shift the radio buttons to be direct children of the body, hiding them visually, and then target #theme-foo:checked ~ * … instead of :root:has(#theme-foo:checked) …. Messy and with some consequences and inconveniences, but generally possible.
    • 将单选按钮移至 body 的直接子级,在视觉上隐藏它们,然后定位 #theme-foo:checked ~ * ……而不是 :root:has(#theme-foo:checked) …。虽然杂乱且有一些后果和不便,但通常是可行的。

I will also use nested selectors in some places in this article (2023-12); but they’re easily flattened if you wish. 我也会在本文的某些地方使用嵌套选择器(2023 年 12 月特性);但如果你愿意,它们很容易被扁平化。

Now to the five techniques. 现在来看看这五种技术。


1. Write it all out the hard way

1. 笨办法:全部手动编写

Old-school and verbose. The most compatible. It’s easy to see how to add more themes than just light and dark. Syntax is verbose. Dark theme value has to be repeated. 老派且冗长。兼容性最好。很容易看出如何添加除浅色和深色之外的更多主题。语法冗长,且深色主题的值必须重复书写。

some-element {
  color: darkred;
  :root:has(#theme-dark:checked) & { color: pink; }
  @media (prefers-color-scheme: dark) {
    :root:has(#theme-auto:checked) & { color: pink; }
  }
}

It’s often structured differently, with the nesting effectively inverted as in the next example, but that’s the gist of it. 它的结构通常会有所不同,嵌套方式实际上与下一个示例相反,但这就是它的核心逻辑。


2. Lots of colour palette variables

2. 大量使用调色板变量

Probably the most popular approach, traditionally. Though I was surprised to realise the implementations only landed in 2014–2017. Sure feels longer ago than that. But it still feels okay to call it “traditional”, because people often used Sass variables to similar effect before. (You didn’t often have multiple themes in those days.) 这可能是传统上最流行的方法。虽然我惊讶地发现其实现直到 2014-2017 年才落地。感觉比那要久远得多。但称其为“传统”依然合适,因为人们过去常使用 Sass 变量来实现类似效果。(那时你并不常有多个主题。)

Works since 2017-04. It’s easy to see how to add more themes than just light and dark. Use-site syntax is ideal. Separating colour definitions from their use location may help or hinder, and may or may not fit nicely into your project. It can encourage some bad and some good patterns, and may yield a little more overhead. (I’m being vague deliberately, and will not elaborate here.) Dark theme value has to be repeated. 自 2017 年 4 月起可用。很容易看出如何添加更多主题。使用时的语法非常理想。将颜色定义与使用位置分离可能会有帮助,也可能会造成阻碍,且不一定适合你的项目。它可能鼓励一些好或坏的模式,并可能产生一点额外的开销。(我故意说得含糊,这里不再赘述。)深色主题的值必须重复书写。

The declaration: 声明如下:

:root { --color-somepurpose: darkred; --color-another: …; ⋮ }
:root:has(#theme-dark:checked) { --color-somepurpose: pink; --color-another: …; ⋮ }
@media (prefers-color-scheme: dark) {
  :root:has(#theme-auto:checked) { --color-somepurpose: pink; --color-another: …; ⋮ }
}

Adding more themes is trivial. And use site: 添加更多主题非常简单。使用时:

some-element { color: var(--color-somepurpose); }

You can also obviously use variables for the next three approaches. 显然,你也可以在接下来的三种方法中使用变量。


3. color-mix() with one variable per theme

3. 使用 color-mix() 和每个主题一个变量

I don’t recall encountering this technique before (actually I’ve been surprised at how little attention color-mix() has been paid), but it works. Basically, define a colour as a mixture of all the colours across all themes, but use a variable to set one of the themes to 100% and the rest to 0%. (In practice, you’re normally just using two themes: light and dark.) 我不记得以前见过这种技术(实际上我很惊讶 color-mix() 受到的关注如此之少),但它确实有效。基本上,将一种颜色定义为所有主题中所有颜色的混合,但使用一个变量将其中一个主题设置为 100%,其余设置为 0%。(实际上,你通常只使用两个主题:浅色和深色。)

Works since 2023-05. It’s easy to see how to add more themes than just light and dark. Syntax is reasonably compact. Syntax is likely to be unfamiliar, but is fairly straightforward. Pity about the interpolation method part that’s currently necessary. Opens up interesting ideas for actually mixing colours/themes. 自 2023 年 5 月起可用。很容易看出如何添加更多主题。语法相当简洁。语法可能比较陌生,但相当直观。可惜目前必须包含插值方法部分。这为实际混合颜色/主题开启了有趣的思路。

What does --dark: 50%; mean? You can play around with the consequences 🎨 Aa on this site! If you do try mixing colours and themes, controlling interpolation is a pain; you have to figure out how to express your desired function mathematically. This is the technique I settled on for my own site, because once I realised the potential, I wanted to play around with mixing themes. I’ve started writing more about it. It’s more interesting/involved than you might imagine. --dark: 50%; 是什么意思?你可以在本站的 🎨 Aa 处尝试其效果!如果你确实尝试混合颜色和主题,控制插值会很痛苦;你必须弄清楚如何用数学方式表达你想要的功能。这是我为自己的网站选择的技术,因为一旦意识到其潜力,我就想尝试混合主题。我已经开始写更多关于它的内容了。它比你想象的更有趣、更复杂。

At the start of the stylesheet: 在样式表开头:

:root { --dark: 0%; }
:root:has(#theme-dark:checked) { --dark: 100%; }
@media (prefers-color-scheme: dark) {
  :root:has(#theme-auto:checked) { --dark: 100%; }
}

Then with each colour we can use color-mix() like so: 然后对于每种颜色,我们可以像这样使用 color-mix()

some-element { color: color-mix(in oklab, darkred, pink var(--dark)); }

And it will be darkred in light mode and pink in dark mode. Mostly you only care about changing colours, but if you wanted to change other things based on the theme you could, by making the variable a number instead of a percentage, and using calc(). (Without having thought through the implications—I wish percentages and numbers were unified, with 100% equivalent to 1.) 这样在浅色模式下它将是 darkred,在深色模式下是 pink。通常你只关心改变颜色,但如果你想根据主题改变其他东西,也可以通过将变量设为数字而不是百分比,并使用 calc() 来实现。(虽然我还没完全想透其中的影响——我希望百分比和数字能统一起来,即 100% 等同于 1。)

Syntax compatibility hazards

语法兼容性风险

Beware of following what the spec allows and what MDN talks of: 注意区分规范允许的内容和 MDN 提到的内容:

  • The interpolation method is optional, defaulting to oklab, but this only happened in late 2025. All major browsers have now shipped it, but by my definition it’s not going to be safe to rely on for another year and a half or so. So for quite some time yet, you’ll still need to write in oklab,.
  • 插值方法是可选的,默认为 oklab,但这直到 2025 年底才实现。所有主流浏览器现在都已经支持了,但按照我的定义,在未来一年半左右的时间里,依赖它还不够安全。所以,在相当长的一段时间内,你仍然需要写上 oklab,
  • MDN browser-compat-data lacks it.
  • MDN 的浏览器兼容性数据中缺少这一项。
  • Lightning CSS lacks it.
  • Lightning CSS 中缺少这一项。
  • The spec permits one or more colour specification, which is handy for mixing more than two themes, but mo
  • 规范允许一个或多个颜色规范,这对于混合两个以上的主题很方便,但是……