Come To The Light-dark() Side


You’d be forgiven for thinking coding up both a dark and a light mode at once is a lot of work. You have to remember @media queries based on prefers-color-scheme as well as extra complications that arise when letting visitors choose whether they want light or dark mode separately from the OS setting. And let’s not forget the color palette itself! Switching from a “light” mode to a “dark” mode may involve new variations to get the right amount of contrast for an accessible experience.

It is indeed a lot of work. But I’m here to tell you it’s now a lot simpler with modern CSS!

Default HTML color scheme(s)

We all know the “naked” HTML theme even if we rarely see it as we’ve already applied a CSS reset or our favorite boilerplate CSS before we even open localhost. But here’s a news flash: HTML doesn’t only have the standard black-on-white theme, there is also a native white-on-black version.

Screenshot of two bare-HTML mini-sites, one light, one dark
We have two color schemes available to use right out of the box!

If you want to create a dark mode interface, this is a great base to work with and saves you from having to account for annoying details, like dark inputs, buttons, and other interactive elements.

Screenshot of two forms, one with elements and background on light mode, the other all in dark mode.
Live Demo on CodePen

Switching color schemes automatically based on OS preference

Without any @media queries — or any other CSS at all — if all we did was declare color-scheme: light dark on the root element, the page will apply either the light or dark color scheme automatically by looking at the visitor’s operating system (OS) preferences. Most OSes have a built-in accessibility setting for your preferred color scheme — “light”, “dark”, or even “auto” — and browsers respect that setting.

html {
  color-scheme: light dark;
}

We can even accomplish this without CSS directly in the HTML document in a tag:

Whether you go with CSS or the HTML route, it doesn’t matter — they both work the same way: telling the browser to make both light and dark schemes available and apply the one that matches the visitor’s preferences. We don’t even need to litter our styles with prefers-color-scheme instances simply to swap colors because the logic is built right in!

You can apply light or dark values to the color-scheme property. At the same time, I’d say that setting color-scheme: light is redundant, as this is the default color scheme with or without declaring it.

You can, of course, control the tag or the CSS property with JavaScript.

There’s also the possibility of applying the color-scheme property on specific elements instead of the entire page in one fell swoop. Then again, that means you are required to explicitly declare an element’s color and background-color properties; otherwise the element is transparent and inherits its text color from its parent element.

What values should you give it? Try:

Default text and background color variables

The “black” colors of these native themes aren’t always completely black but are often off-black, making the contrast a little easier on the eyes. It’s worth noting, too, that there’s variation in the blackness of “black” between browsers.

What is very useful is that this default not-pure-black and maybe-not-pure-white background-color and text color are available as variables. They also flip their color values automatically with color-scheme!

They are: Canvas and CanvasText.

These two variables can be used anywhere in your CSS to call up the current default background color (Canvas) or text color (CanvasText) based on the current color scheme. If you’re familiar with the currentColor value in CSS, it seems to function similarly. CanvasText, meanwhile, remains the default text color in that it can’t be changed the way currentColor changes when you assign something to color.

In the following examples, the only change is the color-scheme property:

Screenshot of code and output area with color-scheme set to light, a large div of background color Canvas with text within set to color CanvasText, and a div within that with the Canvas and CanvasText switched.
Screenshot of code and output area with color-scheme set to dark, the rest of the code is all the same, and the light and dark areas have switched.

Not bad! There are many, many more of these system variables. They are case-insensitive, often written in camelCase or PascalCase for readability. MDN lists 19 variables and I’m dropping them in below for reference.

Open to view 19 system color names and descriptions
  • AccentColor: The background color for accented user interface controls
  • AccentColorText: The text color for accented user interface controls
  • ActiveText: The text color of active links
  • ButtonBorder: The base border color for controls
  • ButtonFace: The background color for controls
  • ButtonText: The text color for controls
  • Canvas: The background color of an application’s content or documents
  • CanvasText: The text color used in an application’s content or documents
  • Field: The background color for input fields
  • FieldText: The text color inside form input fields
  • GrayText: The text color for disabled items (e.g., a disabled control)
  • Highlight: The background color for selected items
  • HighlightText: The text color for selected items
  • LinkText: The text color used for non-active, non-visited links
  • Mark: The background color for text marked up in a element
  • MarkText: The text color for text marked up in a element
  • SelectedItem: The background color for selected items (e.g., a selected checkbox)
  • SelectedItemText: The text color for selected items
  • VisitedText: The text visited links

Cool, right? There are many of them! There are, unfortunately, also discrepancies as far as how these color keywords are used and rendered between different OSes and browsers. Even though “evergreen” browsers arguably support all of them, they don’t all actually match what they’re supposed to, and fail to flip with the CSS color-scheme property as they should.

Egor Kloos (also known as dutchcelt) is keeping an eye on the current status of system colors, including which ones exist and the browsers that support them, something he does as part of a classless CSS framework cleverly called system.css.

Declaring colors for both modes together

OK good, so now you have a page that auto-magically flips dark and light colors according to system preferences. Whether you choose to use these system colors or not is up to you. I just like to point out that “dark” doesn’t always have to mean pure “black” just as “light” doesn’t have to mean pure “white.” There are lots more colors to pair together!

But what’s the best or simplest way to declare colors so they work in both light and dark mode?

In my subjective reverse-best order:

Third place: Declare color opacity

You could keep all the same background colors in dark and light modes, but declare them with an opacity (i.e. rgb(128 0 0 / 0.5) or #80000080). Then they’ll have the Canvas color shine through.

It’s unusable in this way for text colors, and you may end up with somewhat muted colors. But it is a nice easy way to get some theming done fast. I did this for the code blocks on this old light and dark mode demo.

Screenshot of a website split into its dark and light modes, showing code blocks with gentle background colors split across both

Second place: Use color-mix()

Like this:

color-mix(in oklab, Canvas 75%, RebeccaPurple);

Similar (but also different) to using opacity to mute a color is mixing colors in CSS. We can even mix the system color variables! For example, one of the colors can be either Canvas or CanvasText so that the background color always mixes with Canvas and the text color always mixes with CanvasText.

We now have the CSS color-mix() function to help us with this. The first argument in the function defines the color space where the color mixing happens. For example, we can tell the function that we are working in the OKLAB color space, which is a rectangular color space like sRGB making it ideal to mix with sRGB color values for predictable results. You can certainly mix colors from different color spaces — the OKLAB/sRGB combination happens to work for me in this instance.

The second and third arguments are the colors you want to mix, and in what proportion. Proportions are optional but expressed in percentages. Without declaring a proportion, the mix is an even 50%-50% split. If you add percentages for both colors and they don’t match up to 100%, it does a little math for you to prevent breakages.

The color-mix() approach is useful if you’re happy to keep the same hues and color saturations regardless of whether the mode is light or dark.

A screenshot of whimsica11y.net, where the color-mix() method for making the theme is in use

In this example, as you change the value of the hue slider, you’ll see color changes in the themed boxes, following the theme color but mixed with Canvas and CanvasText:

You may have noticed that I used OKLCH and HSL color spaces in that last example. You may also have noticed that the HSL-based theme color and the themed paragraph were a lot more “flashy” as you moved the hue slider.

I’ve declared colors using a polar color space, like HSL, for years, loving that you can easily take a hue and go up or down the saturation and lightness scales based on need. But, I concede that it’s problematic if you’re working with multiple hues while trying to achieve consistent perceived lightness and saturation across them all. It can be difficult to provide ample contrast across a spectrum of colors with HSL.

The OKLCH color space is also polar just like HSL, with the same benefits. You can pick your hue and use the chroma value (which is a bit like saturation in HSL) and the lightness scales accurately in the same way. Both OKLCH and OKLAB are designed to better match what our eyes perceive in terms of brightness and color compared to transitioning between colors in the sRGB space.

While these color spaces may not explicitly answer the age-old question, Is my blue the same as your blue? the colors are much more consistent and require less finicking when you decide to base your whole website’s palette on a different theme color. With these color spaces, the contrasts between the computed colors remain much the same.

First place (winner!): Use light-dark()

Like this:

light-dark(lavender, saddlebrown);

With the previous color-mix() example, if you choose a pale lavender in light mode, its dark mode counterpart is very dark lavender.

The light-dark() function, conversely, provides complete control. You might want that element to be pale lavender in light mode and a deep burnt sienna brown in dark mode. Why not? You can still use color-mix() within light-dark() if you like — declare the colors however you like, and gain much more fine-grained control over your colors.

Feel free to experiment in the following editable demo:

Using color-scheme: light dark; — or the corresponding meta tag in HTML on your page —is a prerequisite for the light-dark() function because it allows the function to respect a person’s system preference, or whichever single light or dark value you have set on color-scheme.

Another consideration is that light-dark() is newly available across browsers, with just over 80% coverage across all users at the time I’m writing this. So, you might consider including a fallback in your CSS for browsers that lack support for the function.

What makes using color-scheme and light-dark() better than using @media queries?

@media queries have been excellent tools, but using them to query prefers-color-scheme only ever follows the preference set within the person’s operating system. This is fine until you (rightfully) want to offer the visitor more choices, decoupled from whether they prefer the UI on their device to be dark or light.

We’re already capable of doing that, of course. We’ve become used to a lot of jiggery-pokery with extra CSS classes, using duplicated styles, or employing custom properties to make it happen.

The joy of using color-scheme is threefold:

  • It gives you the basic monochrome dark mode for free!
  • It can natively do the mode switching based on OS mode preference.
  • You can use JavaScript to toggle between light and dark mode, and the colors declared in the light-dark() functions will follow it.

Light, dark, and auto mode controls

Essentially, all we are doing is setting one of three options for whether the color-scheme is light, dark, or updates auto-matically.

I advise offering all three as discrete options, as it removes some complications for you! Any new visitor to the site will likely be in auto mode because accepting the visitor’s OS setting is the least jarring default state. You then give that person the choice to stay with that or swap it out for a different color scheme. This way, there’s no need to sniff out what mode someone prefers to, for example, display the correct icon on a toggle and make it perform the correct action. There is also no need to keep an event listener on prefers-color-scheme in case of changes — your color-scheme: light dark declaration in CSS handles that for you.

Three examples of mode switches, each with the three options of Auto, Light and Dark. Buttons, a fieldset with radio buttons, and a select element.

Adjusting color-scheme in pure CSS

Yes, this is totally possible! But the approach comes with a few caveats:

  • You can’t use

    The last benefit of aria-pressed is that we can also target it for styling purposes:

    button[aria-pressed="true"] {
      background-color: transparent;
      border-width: 2px;
    }

    Finally, we have a nice little button switcher, with its state clearly shown and announced, that remembers your choice when you come back to it. Done!

    Outroduction

    Or whatever the opposite of an introduction is…

    …don’t let yourself get dragged into the old dark vs light mode argument. Both are good. Both are great! And both modes are now easy to create at once. At the start of your next project, work or hobby, do not give in to fear and pick a side — give both a try, and give in to choice.

    Darth Vader clenching his fist, saying “If you only knew the power of the Dark Side.”



Read more