light-dark() has been swapping colors for a while. Now it swaps images too: give it two gradients, two url()s, or an image and none, and the browser picks one based on the element's color scheme. As of September 14, 2026 that works in Chrome, Edge, Firefox and Safari, which makes it Baseline newly available.
The landscape in the demo above has no image files and no theming JavaScript. The sky, the sun that turns into a moon, the stars, the clouds, the hills and the lit windows are all background-image: light-dark(day, night). The JavaScript in the demo only does two things: it changes the scene's color-scheme when you click Day or Night, and it writes the code panel.
Why this matters
Dark mode always had a gap. Colors were easy with light-dark() or custom properties, but any time an image had to change (a logo, a hero gradient, a list bullet, a decorative background) you went back to @media (prefers-color-scheme: dark) and duplicated the rule.
A media query only knows what the user picked in their system or browser. It doesn't know that one section of your page is forced to color-scheme: dark. light-dark() resolves against the color scheme of the element it's applied to, so the same rule does the right thing in a dark sidebar sitting inside a light page. The three logo tiles at the bottom of the demo share one CSS rule: one tile is color-scheme: light, one is dark, and one is light dark, which follows your system.
How it works
The spec now defines two forms. The color form you already know, and an image form where each argument is an <image> or none:
:root {
color-scheme: light dark;
}
.hero {
background-image: light-dark(
linear-gradient(#38bdf8, #fde68a), /* light */
linear-gradient(#050816, #4c1d95) /* dark */
);
}
The first argument is used when the element's color scheme is light. The second is used when it's dark. As MDN notes, you have to opt in with color-scheme: light dark (usually on :root). Without it, the page stays light and you always get the first image.
To force one area to a scheme, set color-scheme on that element. That's exactly what the demo's switch does:
.scene[data-scheme="light"] { color-scheme: light; }
.scene[data-scheme="dark"] { color-scheme: dark; }
none is valid on either side. The spec defines it as a fully transparent image with no natural size, and in Chrome the computed value reads image(rgba(0, 0, 0, 0)). It's handy for layers that should only appear in one scheme, like the stars:
.stars {
background-image: light-dark(none, radial-gradient(1px 1px at 20px 30px, #fff 50%, transparent 51%));
}
It works anywhere an image goes, not only on backgrounds. The spec's own example uses it for list bullets:
ul.fancy {
list-style-image: light-dark(url("light.svg"), url("dark.svg"));
}
I checked all of these snippets in Chromium by reading getComputedStyle() under a light and a dark scheme. Each time the browser returned the image I expected.
The gotcha that cost me a layer
My first version of the demo had no stars. I had written this:
/* Does NOT work: three images on the dark side */
.stars {
background-image: light-dark(none, radial-gradient(…), radial-gradient(…), radial-gradient(…));
}
Each side of light-dark() takes a single image. A comma-separated list makes the whole value invalid, and the property falls back to none. The fix is one light-dark() per layer:
.stars {
background-image:
light-dark(none, var(--star1)),
light-dark(none, var(--star2)),
light-dark(none, var(--star3));
}
With var() it's harder to spot. The browser can't reject the declaration while parsing because it doesn't know what the variables contain yet, but in my test the result was still none.
Browser support
According to MDN's browser compatibility data, the image form shipped in Chrome and Edge 150, Firefox 150 and Safari 27. The color form has been around much longer: Chrome 123, Firefox 120 and Safari 17.5. If you test only with colors in an older browser, you may wrongly assume images work as well.
The demo checks support with:
CSS.supports('background-image', 'light-dark(none, linear-gradient(red, blue))');
If that returns false, the demo shows a notice and switches to a plan B: the same gradients chosen with a data-resolved attribute. In plain CSS, the fallback can be an @supports block:
.hero { background-image: url(light.png); }
@supports (background-image: light-dark(none, none)) {
.hero { background-image: light-dark(url(light.png), url(dark.png)); }
}
The fade you see when you switch between day and night isn't part of light-dark(). It comes from a View Transition (document.startViewTransition), and the demo skips it when prefers-reduced-motion is on.
What to keep an eye on
- Older browsers in your traffic. This is newly available, not widely available. Keep a fallback for anything important, like a logo that disappears on a dark background.
- Image loading. Before you swap two heavy raster images this way, check in your own DevTools Network panel what each browser downloads and when.
- Where you use it. Logos, illustrations and decorative gradients are the obvious first targets. Anything that used to need a duplicated rule inside a
prefers-color-schemequery is worth reviewing.