Why images fail in dark mode
A classic example: the company logo, black on a white background, was created for the classic light theme. In dark mode it gets a white padding halo, because many logos have the background color baked into the file. Or a UI icon with dark lines becomes nearly invisible on a dark theme.
Dark mode has established itself in 2026 as a default expectation: 60–70% of users enable it on mobile, 35–45% on desktop (PostHog telemetry 2025). Fail to actively plan images for dark mode and your branding is structurally worse displayed.
Technique 1: prefers-color-scheme in the picture tag
The most robust solution — and supported in all mainstream browsers since 2024 — is the media attribute on <source> elements:
<picture> <source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)"> <img src="logo-light.svg" alt="Brand"> </picture>
Strengths: declarative, robust, no JavaScript, works with lazy loading. Evaluated automatically and reactively on theme change in the browser. Weaknesses: maintaining two separate asset files. With many logos this quickly gets laborious.
When: for logos and complex brand marks where the dark variant should look content-wise different (a knockout version with a different symbol, lighter colors instead of inverted, etc.).
Technique 2: SVG with currentColor
For UI icons and simple logos, the most elegant solution is an SVG that inherits its color from CSS:
<!-- SVG: no baked-in fill -->
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L2 22h20L12 2z"/>
</svg>
/* CSS: a single value for both themes */
.icon { color: var(--text); }
:root[data-theme="light"] { --text: #111; }
:root[data-theme="dark"] { --text: #eee; }Strengths: a single file, automatic adjustment to theme variables, compatible with the Icon Studio workflow. Weaknesses: only sensible for mono- and bi-chrome icons. Doesn't work with embedded raster images.
When: UI icon sets, mono logos, line logos, anything SVG-based with clean lines. More background in our SVG/PNG/JPG icon post.
Technique 3: CSS filter as an emergency solution
For legacy images without a dark variant, a CSS filter helps:
@media (prefers-color-scheme: dark) {
.legacy-logo { filter: invert(1) hue-rotate(180deg); }
}Strengths: immediately available without an asset change. Weaknesses: the result rarely looks good — gradients rotate wrongly, anti-aliasing becomes visible. When: only as a temporary fallback, not as the default solution.
Technique 4: photos with an adjustable background
With product photos the problem is different: a product photo on a white background looks like a lamp sticker in dark mode. Three solutions:
- Transparent background (PNG/WebP alpha). The elegant path — the product stands directly on the theme background. Requires a background-removal pipeline.
- A photo variant per theme. A photo with a light and a photo with a dark background, selected via
<picture>+media. High asset effort, best control. - A padding box with a theme color. The photo stays on a white background, but CSS surrounds it with a 20 px box in the theme color. Visually the compromise.
Logo-specific workflow
A modern brand logo is maintained today in two or three variants:
- Wordmark positive: dark on light. The classic default variant.
- Wordmark negative: light on dark. The dark-mode variant.
- Mono variant: shape only, single-color. For watermarks, stamps, embossing.
If you build a brand-asset system, derive all three from the same master file. SVG export from Figma can be automated per theme with layer visibility.
Performance implications
Dark-mode images cost bytes. Three practices save them:
- SVG vars beat bitmap pairs. One SVG file with
currentColorreplaces two PNG variants. - Format cascade per theme. Within the
<picture>you can combine both cascades: Light/AVIF, Light/WebP, Light/JPG, Dark/AVIF, Dark/WebP, Dark/JPG. Extends the tag complexity but keeps bytes minimal. - Preload only the relevant theme. With server-side theme detection (cookie, User-Agent client hints), preload the matching image directly — saves an unnecessary request.
More on LCP tuning in our Core Web Vitals post.
Theme reactivity without a flash
A subtle trap: if the theme is only set via JavaScript after the first render (the classic FOUC problem), you briefly see the light-theme image on page load, then it switches to the dark-mode image. Solution: theme detection in the HTML server render (the User-Agent client hint "Sec-CH-Prefers-Color-Scheme" has been available in Chromium since 2024) or a synchronous inline-script hack before the body render.
Recommendation matrix
- UI icons: SVG with currentColor. One file, both themes.
- Logos: picture tag with two SVG variants (light/dark).
- Hero images with a photo: picture tag with two photo variants.
- Product photos: transparent background (PNG/WebP alpha), one file is enough.
- Legacy logos without a dark variant: short-term a CSS filter, long-term a redesign.
Sources
MDN — prefers-color-scheme · MDN — picture, Art Direction · web.dev — prefers-color-scheme · MDN — Client Hints (Sec-CH-Prefers-Color-Scheme) · W3C SVG 2 — Specifying Paint · MDN — CSS filter · PostHog — Dark Mode Adoption.