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

27 points by chrismorgan a day ago on lobsters | 12 comments

vbernat | a day ago

Isn't possible to simplify with:

:root {
  color-scheme: light dark;
}
:root:has(#theme-dark:checked) {
  color-scheme: dark;
}
:root:has(#theme-light:checked) {
  color-scheme: light;
}

Then only rely on @media (prefers-color-scheme: XXX)?

[OP] chrismorgan | 19 hours ago

Alas, no. Media queries are global, so setting color-scheme doesn’t influence (prefers-color-scheme).

In theory, @container style(color-scheme: dark) might work, though at present you’d need to use a custom property (e.g. @container style(--dark)) and wait for Safari to allow the document element to be a container and wait for Firefox 151.

vbernat | 18 hours ago

I see that light-dark() follow color-scheme. So, it would work if you only use light-dark() and not media queries.

kosayoda | a day ago

I was surprised to see my favorite method not listed here, which uses some less known rules of CSS custom properties: https://kosalab.dev/posts/no-duplication-css-theming

The approach does not require duplication, is flexible enough to support theming, is widely supported (afaik), and works with any CSS value, not just colors!

[OP] chrismorgan | a day ago

Ah, the space hack. I had quite forgotten about that nasty thing that broke so many tools that had made reasonable assumptions about the insignificance of whitespace…

I’ll consider adding something about it on Monday.

aziis98 | a day ago

Instead of color-mix I prefer using relative colors with the hsl or oklab calc syntax. This way you can easily get color variations with the same saturation/lightness/hue.

I personally don't like the light-dark function as it's a bit too limiting (from this point of view I prefer the new if syntax) and I prefer having theme colors defined together and not scattered around in the stylesheet. Also a "good" dark theme variant often needs special rules and changes here and there and not just color changes and the light dark function is far too specific for this use cases

[OP] chrismorgan | a day ago

I forgot about relative colours. Probably because I wrote that technique off long ago because it’s sorely limiting—colour just doesn’t work like that, perceptual colour space or no. The best results never follow exact RGB/LCH/LAB/whatever values. They’re far too limiting. Also yellow is a pain. And if you’re trying to use the same chroma/hue/saturation values with two wildly different lightness values, for light and dark… well, these colour spaces are not at all symmetrical. It just doesn’t work very well. Acceptable colour schemes can be generated numerically in a decent colour space, but good colour schemes are not so generated.

I’m not saying relative colours are worthless, but for this sort of problem, I don’t think they’re a good solution. They’re probably not that far above keyframes rules in my mind.

Perhaps I should still add a section for it.

toastal | 22 hours ago

I appreciate that the writer bothered to go into the more-than-2 theme scenario, since it isn’t trivial or supported out-of-the-box like the binary light/dark. It seems that while we could have had color-scheme: light dark neon, we don’t. Way back we had multiple style sheets too (which you can still see in since v3.x Fx’s menu @ View > Page Style > …), but the alternate stylesheet API didn’t save your selection so refreshes you had to reselect, get an add-on, or the site dev needed to handle it for you (ironically what you need to support multiple themes now anyhow)—& isn’t even supported in most browsers anymore. Why do I care? For one we used to/could have more than a binary option, Bring back a OLED black color schemes by offering an extra scheme! I don’t like the way these muddy tinted grays look on so many sites on my screens—& it requires more brightness for me to increase the perceived contrast.

deivid | a day ago

What about images per theme? I use <picture> with the media="(prefers-color-scheme: dark)" attribute to do dark/light. I find it a shame, because it means I can't (or, don't know how), do a CSS only theme switcher for my site.

I embed some CSS into my SVGs, so that's also a problem -- they can read the system theme and change their own colors, but (at least for an SVG in an <img> tag), they can't read the state of some radiobutton.

SVGs I could fix by embedding them directly into my page, which sucks, but it's an option. For the other images, I don't knoww.

I have the light and dark mode alternatives in different source paths, and use display: none; to hide them depending on the chosen color scheme:

img[src^="/assets/img/dark"],
img[src^="/assets/img/dark"] + figcaption {
  display: none;
}


@media (prefers-color-scheme: dark) {
  img[src^="/assets/img/dark"],
  img[src^="/assets/img/dark"] + figcaption {
    display: inherit;
  }

  img[src^="/assets/img/light"],
  img[src^="/assets/img/light"] + figcaption {
    display: none;
  }
}

I talk about this a little bit in this post.

[OP] chrismorgan | 2 hours ago

<picture> has the advantage that it definitely doesn’t need to download more than one source.

Your approach may download both images, and will definitely present both in a non-CSS environment.

[OP] chrismorgan | 18 hours ago

In the previous iteration of my website, which came in the fairly early days of (prefers-color-scheme), I required JS to manually select a theme, and the JS would then rewrite media attributes (for stylesheets or picture sources) from screen and (prefers-color-scheme: dark) to either screen (when dark; always matches on screens) or not all (when light; never matches). I described the approach in some detail: https://chrismorgan.info/blog/dark-theme-implementation/.

If I start wanting different sources for light and dark images again, I’ll probably resurrect that, and just live with the sad reality that image sources may mismatch, sans-JS.

Fundamentally I think we still need one more piece. The media attribute doesn’t cut it because of being global. But I think a container attribute would be a hard sell since you couldn’t find the source to use until you’ve started rendering. I’m not sure if it will be solved any time soon.