light-dark()
makes it easy to build websites that respect the user's preferred color scheme while also providing the ability to override the color scheme without code duplication. Be sure to check out the demo at the bottom!
Before we dive into light-dark()
, let's take a quick detour to understand color-scheme
.
Color scheme
#Browsers have a built-in "color scheme" mechanism, which can be specified using the color-scheme
CSS property or the corresponding meta tag.
The color-scheme
can be set to either "light" or "dark" to indicate which mode the page supports. It can also be set to a space-separated "light dark" value, which indicates that the page supports both modes.
Typically, you would set this on the html
element so that it gets inherited by everything on the page.
html {
color-scheme: light dark;
}
This is kinda like saying, "Hey browser, this page supports both light and dark modes, so go look at the user's preferences and decide which mode to actually use."
The browser then uses the resolved color scheme value to determine the default colors for things like the page background, text color, scrollbars, form controls, and system colors.
It's worth clarifying at this point that color-scheme
is a completely separate concept from the prefers-color-scheme
media query, which you might have used in the past to implement a dark mode. The media query is only concerned with looking at the user preferences. Even if you're using the media query, it should be paired with the color-scheme
property so that scrollbars and form controls get more appropriate default styles.
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
Everything we've learned about color-scheme
thus far has been widely available across browsers for many years now.
Now that we understand color-scheme
, we can look at the new light dark()
feature!
Enter light-dark()
#You might have noticed that, up until now, color-scheme
is somewhat of a one-way signal. Browsers can use color-scheme
to adjust the default colors (and system colors), but authors cannot use it in their own code to adjust their custom styles.
This is where light-dark()
comes in. From the spec:
System colors have the ability to react to the current used color scheme value. The
light-dark()
function exposes the same capability to authors.
light-dark()
is a CSS function that accepts two color values and returns one of them based on the used color-scheme
. The following snippet will make all links be DeepPink
in light mode and HotPink
in dark mode.
:any-link {
color: light-dark(DeepPink, HotPink);
}
It's a clean and concise way of doing something that would have required multiple CSS rules and media queries in the past.
The two colors don't need to be hardcoded; they can reference custom properties (aka "CSS variables"). And they can also be referenced by
other custom properties (as we'll see later).
:any-link {
color: light-dark(var(--accent-light), var(--accent-dark));
}
light-dark()
has been newly available across modern browsers since early 2024. For older browsers, we can transpile our code using LightningCSS or a PostCSS plugin.
Themed variables using light-dark()
#To get the most out of light-dark()
, it is best used globally for the purpose of setting up themed custom properties.
Let's start by defining three custom properties: --accent
, --bg
, and --fg
.
html {
--accent: light-dark(DeepPink, HotPink);
--bg: light-dark(White, Black);
--fg: light-dark(Black, White);
}
Don't pay mind to the actual values in this example. It's probably not desirable to use the built-in named CSS colors. Usually, you'll want to agonize over picking the right colors. I'll leave that as an exercise for the reader. (If you need a little push, Open Props provides a few different color scales which can be a good starting point for your color system.)
Once the custom properties are set up, we can use them everywhere in our CSS. Notice how we don't need to repeat the raw color values. Because these custom properties were set up using light-dark()
, the whole page automatically works in both light and dark modes without needing to adjust anything at the component level.
html {
accent-color: var(--accent);
background-color: var(--bg);
color: var(--fg);
}
*:focus-visible {
outline: 2px solid var(--accent);
}
:any-link {
color: var(--fg);
}
button {
color: var(--fg);
border: 1px solid var(--fg);
}
.surface {
background-color: var(--bg);
}
/* and so on... */
Overriding color-scheme
#So far, we've only seen how light-dark()
can replace a media query. This is great, but we usually want more: the site should respect the user's preference by default, but the user should also be able to override the default color scheme.
The key insight here is that light-dark()
is tied to the color-scheme
, rather than to user preference. While setting color-scheme: light dark
will match the user preference, we can also override it manually.
Let's say our website has a theme toggle, which sets a data-color-scheme
attribute on the <html>
element.
/*
* Call this function from your theme toggle.
* @example
* setColorScheme("light");
* setColorScheme("dark");
*/
function setColorScheme(mode) {
document.documentElement.dataset.colorScheme = mode;
}
Now, we can use this data attribute as a CSS selector to change our color-scheme
.
html {
color-scheme: light dark;
&[data-color-scheme="light"] {
color-scheme: light;
}
&[data-color-scheme="dark"] {
color-scheme: dark;
}
}
Changing the color-scheme
will automatically result in all instances of light-dark()
returning the appropriate color value. In this instance, we are setting color-scheme
on the same element (<html>
) where our custom properties are defined. However, light-dark()
also works for descendant elements because color-scheme
is an inherited property.
That's really all the code we need to implement light and dark modes. It respects the user preference (without a media query!) and can also be overridden.
For comparison, implementing the same functionality without light-dark()
would look something like the following. We would need to define the custom properties four different times, two of which are entirely duplicate copies of each other.
/* without light-dark() */
html {
color-scheme: light dark;
@media (prefers-color-scheme: light) {
--accent: DeepPink;
--bg: White;
--fg: Black;
}
@media (prefers-color-scheme: dark) {
--accent: HotPink;
--bg: Black;
--fg: White;
}
}
html {
&[data-color-scheme="light"] {
color-scheme: light;
--accent: DeepPink;
--bg: White;
--fg: Black;
}
&[data-color-scheme="dark"] {
color-scheme: dark;
--accent: HotPink;
--bg: Black;
--fg: White;
}
}
Meanwhile, with light-dark()
, we can keep all our custom properties neatly defined in a single place.
/* with light-dark() */
html {
--accent: light-dark(DeepPink, HotPink);
--bg: light-dark(White, Black);
--fg: light-dark(Black, White);
}
The future is bright (and dark)!
To the CodePen!
#Let's take everything we've learned about light-dark()
and put it to use.
This demo shows a tri-state toggle that works only by changing the document's color-scheme
. All of the theme-related CSS is wrapped under @layer theme
.
Further reading
#If you're anything like me, you learn new topics by absorbing the same information from several different sources. So here are a few good links related to color-schem
e and light-dark()
:
- Easy Light-Dark Mode Color Switching with light-dark() by Bramus
- Come To The Light-dark() Side by Sara Joy
- What I've learned about CSS color-scheme and friends by Anne Sturdivant
- Opting Into a Preferred Color Scheme: the color-scheme property from CSSWG (this is a fun read!)
- The Perfect Theme Switch Component by Aleksandr Hovhannisyan
Mayank selected For the Gworls for an honorary donation of $50
For The Gworls is a Black trans-led collective that raises funds to directly help Black trans people pay for rent assistance, gender-affirming surgeries, and critical medical expenses.