light-dark() now takes images, not just colours
The light-dark() function takes two values and returns the first when the element is in light mode and the second when it is in dark mode. When it first shipped it took colours only. Now it takes images too, which makes swapping illustrations with the colour scheme much simpler.
The whole trick
:root {
color-scheme: light dark;
}
.hero {
background-image: url(day.svg);
background-image: light-dark(url(day.svg), url(night.svg));
}Three things to note:
color-scheme: light darkis required.light-dark()does not read the operating system by itself. It follows the color-scheme applied to the element, and this line lets the page follow the system setting.- The first
background-imageline is the fallback. A browser that doesn't understandlight-dark()with images skips the second line and keeps the day image. - No more
@media (prefers-color-scheme: dark)block repeating the selector. The light and dark values sit side by side on one line.
Example 1: Day and night backgrounds
The box below uses exactly the two background-image lines above, with two inlined SVG images. Press a button to change the color-scheme of the box alone, or pick “Follow page” to let the box follow your device’s light or dark mode. In Chrome, transition: background-image also gives a cross-fade between the two images.
The main CSS:
:root {
color-scheme: light dark;
}
.scene {
background-size: cover;
background-position: center;
background-image: url(day.svg); /* older browsers stop here */
background-image: light-dark(url(day.svg), url(night.svg)); /* newer browsers pick by color-scheme */
color: light-dark(#3b2416, #f3e6d3);
transition: background-image 600ms ease, color 600ms ease;
}
@media (prefers-reduced-motion: reduce) {
.scene { transition: none; }
}The buttons only set element.style.colorScheme on the box. Choosing the image is still light-dark()’s job.
Example 2: Not just backgrounds
light-dark() works from the color-scheme of each element, so you can force an area to stay light or dark with color-scheme on that element. The function also works in other properties that take images: list-style-image for bullets and content for an icon before a heading.
.panel {
background: light-dark(#fffaf3, #1d1b22);
color: light-dark(#3b2416, #ece2d6);
border: 1px solid light-dark(#ecdcc8, #34303d);
}
/* Illustration: background-image */
.panel .art {
background: center / cover no-repeat;
background-image: light-dark(url(day.svg), url(night.svg));
}
/* Bullets: list-style-image */
.panel ul {
list-style-image: light-dark(url(sun.svg), url(moon.svg));
}
/* Icon before the heading: content */
.panel h3::before {
content: light-dark(url(sun.svg), url(moon.svg));
margin-right: .4rem;
}Three boxes share that CSS and give three different results:
<div class="panel" style="color-scheme: light">…</div> <!-- always light: day image, sun bullets -->
<div class="panel" style="color-scheme: dark">…</div> <!-- always dark: night image, moon bullets -->
<div class="panel">…</div> <!-- follows the page, inherited from :root -->Example 3: Does your browser support it?
Use CSS.supports() to check directly. The box below runs the check in your own browser.
To run it yourself, paste this into the console:
const img = 'light-dark(url(a.svg), url(b.svg))';
for (const prop of ['background-image', 'list-style-image', 'mask-image', 'content']) {
console.log(prop, CSS.supports(prop, img) ? 'supported' : 'not supported');
}
console.log('color (colours only, already shipped)', CSS.supports('color', 'light-dark(white, black)'));In CSS you can do the same with @supports (background-image: light-dark(url(a.svg), url(b.svg))).
Things to watch
- If your design system uses an attribute such as
data-theme, make sure it also setscolor-scheme. The demos above use daisyUI, which already does, so they follow its light/dark toggle with no extra line. - Give both images the same size and layout so the switch doesn't jump.
- An SVG can carry its own animation and respect
prefers-reduced-motioninside the file, like the sun and stars in example 1.
Browser support. light-dark() taking images is new. Example 3 tells you exactly how far your current browser goes. Where it isn't supported yet, keep the fallback declaration first, as in the code above.