Theming with CSS in 2025: from Dark Mode to color-scheme and light-dark()
Table of contents
A few years ago, dark mode burst onto the scene — originally meant to reduce visual fatigue for users (after all, the web is a network of documents designed to be read on bright screens) — but it quickly became popular for its clean aesthetics and elegant feel. Companies like Google and Facebook, and operating systems like iOS and Android, soon adopted it. The trend spread widely and became just another design tool.
Many sites — including this one — offered their own dark version, letting users choose their preferred appearance, and everything was great. It introduced a few technical challenges, such as designing a color palette robust enough to support both versions, or persisting the user’s choice — but at the same time, it was exciting.
As for implementation, the best way used to be taking advantage of the cascade — the “C” in CSS — by adding a class or attribute to a top-level element like html or body, as we discussed in ES: How to Create Different Themes in CSS.
But today, we can simplify that approach thanks to new CSS capabilities like prefers-color-scheme, color-scheme, and the light-dark() function.
In system and browser preferences, users can choose betweenlightanddark, withlightbeing the default.
prefers-color-scheme
This is a @media directive that lets us detect the user’s preferred color scheme — so we can react to it accordingly.
@media (prefers-color-scheme: light) {
:root {
--bg-color: #ffffff;
--front-color: #ff0066;
}
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #171115;
--front-color: #ddccbb;
}
}
You can be explicit and assign variables for each version, or declare a default and override it for the opposite. The result is the same. The key is having a well-structured color palette that can be channeled through custom properties and reassigned per variant. That’s the idea — no more detecting, saving, or persisting user preferences manually. And any change in the system color scheme updates in real time.
Images
If your site is mostly text and vector elements, you’ll have no issues. But if you use images that don’t contrast well against both backgrounds, the picture element is very helpful. The source tag accepts a media attribute — identical to CSS @media — so you can use prefers-color-scheme like this:
<source srcset="alternative.png" media="(prefers-color-scheme: dark)" />
Demo
color-scheme
This property tells the browser which color schemes a document or element supports, and how system-level UI (like form controls, scrollbars, and autofill) should be rendered.
It’s interesting because you can control it globally or very granularly:
:root {
color-scheme: light dark;
}
.element {
color-scheme: light dark;
}
The only keyword deserves special attention — it locks the color scheme so the browser won’t switch automatically.
form {
color-scheme: only dark;
}
Demo
You can also declare the color scheme in the document’s <head> using:
<meta name="color-scheme" content="dark light" />
While color-scheme can be combined with prefers-color-scheme, I personally prefer pairing it with the light-dark() function.
light-dark
This function takes two color values.
The first applies when the color scheme is light (or undefined), and the second applies when it’s dark.
/* To make light-dark() work, you must declare color-scheme: light dark first */
:root {
color-scheme: light dark;
}
.element {
color: light-dark(cyan, lime);
}
You can of course use variables:
.element {
color: light-dark(var(--color-light), var(--color-dark));
}
This approach is more direct and explicit than using prefers-color-scheme.
It’s also flexible — you can apply it to any element independently, and since it accepts any color, you can easily create exceptions.
The downside is that even though variables remain centralized in :root, the implementation gets scattered across your codebase.
Demo
If
If you think about it, light-dark() behaves like an if/else.
So alternatively, we could use the if() function — though support is still limited, so I’m not taking it too seriously yet.
This function lets you apply one value or another depending on a style, media, or feature test. For example:
.element {
color: if(media(prefers-color-scheme: dark): purple; else: #f06);
}
It’s a bit verbose, and the previous approaches are usually better — but it’s interesting and powerful nonetheless.
Demo
Conclusion
CSS is an evolving language — it adapts quickly to our needs, which is, in my opinion, the key to its success. Which approach you have to use will depend on your needs.
Sometimes the spotlight shines on the flashier features like view transitions or if(), leaving others in the shadows — but those can be just as impactful.
In this case, color-scheme and light-dark() solve a complex problem in a beautifully simple way.
That said, while we’ve focused on implementation, it’s equally essential to plan and design a strong color palette. Without that foundation, the result can easily become inconsistent — or even chaotic.
Resources
comments powered by DisqusIf you find it interesting
If you have any doubt or you want to chat about this topic, as if you find interesting the content or our profiles and you think we could build something together, do not hesitate to contact us trough the email address hola@mamutlove.com