Dark Mode: It Is Not Just Inverting Colors
Dark mode is expected by users. Implementing it naively leads to low contrast and eye strain. How to engineer a semantic color system that adapts gracefully.
The Expectation of Darkness
In 2020, Dark Mode was a “Nice to have”. In 2025, it is a requirement. iOS, Android, macOS, and Windows all support system-wide dark mode. If your user has their phone set to Dark Mode (to save battery or save their eyes at night), and they open your website and get hit with a blast of #FFFFFF white light… They close the tab. It is physically painful. Dark Mode is not a feature; it is User Empathy.
Why Maison Code Discusses This
At Maison Code, we strive for “The Seamless Experience”.
We ensure our clients’ sites respect the user’s system preferences automatically (prefers-color-scheme).
But more importantly, we understand that Dark Mode is a Branding Challenge.
Luxury brands rely on specific colors. How do you translate “Chanel Black on White” to Dark Mode without losing the identity?
We handle this semantic translation daily. We don’t just “invert colors”. We “re-interpret” the brand for low-light environments.
The Engineering Strategy: CSS Variables + HSL
(See CSS Variables).
The naive approach is:
body.dark { background: black; color: white; }.
This looks terrible. Pure black (#000) causes “smearing” on OLED screens when scrolling. Pure white text (#FFF) on pure black causes eye strain (halation).
The Semantic Approach: We define colors using HSL (Hue, Saturation, Lightness). This allows us to modify lightness programmatically.
:root {
/* Brand Hue (e.g. Blue) */
--brand-h: 210;
--brand-s: 100%;
/* Light Mode */
--bg-l: 100%;
--text-l: 10%;
--bg-color: hsl(var(--brand-h), 10%, var(--bg-l));
--text-color: hsl(var(--brand-h), 10%, var(--text-l));
}
[data-theme="dark"] {
/* Dark Mode: Just flip the lightness */
--bg-l: 10%;
--text-l: 90%;
}
Notice we didn’t just pick “Black”. We picked a “Very Dark Blue” (hsl(210, 10%, 10%)).
This preserves the brand tint even in dark mode. It feels richer and more premium than flat grey.
Technical Implementation: The Toggle & The Flicker
The hardest part of Dark Mode is the FOUC (Flash of Unstyled Content) or Flash of White.
- User loads page (Server sends HTML).
- Browser renders White background default.
- JS loads, reads
localStorage, sees “Dark”. - JS adds
.darkclass. - Page flashes to Black. This split-second flash destroys the user experience.
The Fix: A Blocking Script in <head>.
You must inject a tiny script before the CSS loads.
<head>
<script>
(function() {
const stored = localStorage.getItem('theme');
const system = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (stored === 'dark' || (!stored && system)) {
document.documentElement.classList.add('dark');
}
})();
</script>
</head>
Because this script is in the head and blocking, the browser executes it before the first paint. The page renders initially as Dark. Zero flicker.
Designing for Depth
In Light Mode, we use Shadows to show depth (Cards floating on background). In Dark Mode, shadows are invisible (Dark shadow on dark background). Solution: Use Lightness to show depth.
- Background: Level 0 (Darkest).
- Card: Level 1 (Slightly lighter).
- Modal: Level 2 (Lighter).
- Button: Level 3 (Lightest). Instead of “Shadows”, we use “overlays” or slight background color shifts.
Images and Dark Mode
Bright images in Dark Mode can be jarring. Tip: Reduce the brightness and contrast of images slightly when in dark mode.
[data-theme="dark"] img {
filter: brightness(0.8) contrast(1.2);
}
This makes the visuals blend better with the dark interface.
The Skeptic’s View
“Dark mode is a fad. I like white paper.” Counter-Point:
- Battery: On OLED screens (iPhone X+), dark pixels are off. Dark mode saves ~30% battery life.
- Health: Reducing blue light exposure at night helps sleep hygiene.
- Choice: Give the user the choice. If you force Light Mode, you are arrogant.
FAQ
Q: Should I use a toggle button?
A: Yes. Auto-detect prefers-color-scheme first, but allow user override. Place it in the Footer or Header.
Q: Inverting logos?
A: If your logo is black text, it disappears in dark mode.
You need an SVG logo that inherits currentColor, or you need to swap the image source (logo-dark.png).
11. Advanced Icon Handling (SVGcurrentColor)
Stop exporting black PNG icons.
Use Inline SVGs with fill="currentColor".
This makes the icon inherit the text color of its parent.
If the parent text is white (Dark Mode), the icon is white.
If the parent text is black (Light Mode), the icon is black.
This removes the need for icon-dark.svg and icon-light.svg duplicates.
One asset. Infinite themes.
12. Persisting Preference without FOUC
The “Cookie” vs “LocalStorage” debate.
If you use Server Side Rendering (Next.js), localStorage is not available on the server.
So the server renders Light Mode. Client hydrates Dark Mode. Flicker.
Solution: Cookies.
When the user toggles the theme, set a Cookie theme=dark.
Read this cookie in middleware.ts or getServerSideProps.
Inject <html class="dark"> on the server.
Now the HTML arrives already Dark. Perfect First Paint.
13. Accessibility: Contrast Ratios (WCAG)
Dark Mode is often harder to read for astigmatism.
“Halation” (halo effect) occurs with white text on a black background.
Rule: Never use pure black (#000000).
Use Dark Grey (#121212 or similar). It reduces eye strain.
Testing: Use the Chrome DevTools “Contrast” picker.
Ensure your text meets AA Standard (4.5:1).
Often, your “Brand Blue” works on White but fails on Black.
You need --brand-text-dark (a lighter blue) specifically for dark mode text.
14. Handling System Theme Changes Dynamically
User changes their Mac from Light to Dark while browsing.
Does your site adapt instantly?
Use the change event listener on matchMedia.
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
const newColorScheme = e.matches ? "dark" : "light";
// Only update if user hasn't explicitly overridden logic
if (!localStorage.getItem('theme')) {
document.documentElement.classList.toggle('dark', e.matches);
}
});
This enables that “Magical” feel where the website breathes with the OS.
15. OLED Optimization (True Black vs Grey)
There is a debate.
Pure Black (#000000) turns pixels off. It saves maximum battery.
Dark Grey (#121212) is better for text readability and reduces OLED smearing (purple trail when scrolling).
Maison Code Verdict: Use Dark Grey for Surfaces (Cards, Backgrounds) and Pure Black for Borders or negative space.
This gives you the “Premium Feel” without the visual artifacts.
We also use meta name="theme-color" to tint the Safari Browser Bar to match your background, creating a borderless app-like experience.
16. Server-Side Theme Detection (Client Hints)
Cookies are good. Client Hints are better.
Sec-CH-Prefers-Color-Scheme.
If the browser sends this header, the server knows the user prefers Dark Mode without reading a cookie.
This is the holy grail.
It allows the very first visit (no cookie) to be perfectly themed.
Maison Code implements this bleeding-edge standard for our clients to guarantee Zero-Flicker on the first byte.
17. Why Maison Code?
At Maison Code, we don’t just “flip the colors”. We re-engineer the Brand Identity for low-light environments. We audit contrast ratios to ensure accessibility compliance (ADA/WCAG). We implement the “Flicker-Free” script to ensure your users never get flashed. We believe that Dark Mode is an empathy exercise. It shows you care about how your user consumes your content.
17. Conclusion
Dark Mode is not about changing colors. It is about checking contrast ratios (WCAG AA), managing depth perception, and respecting user context. Implemented well, it feels like a luxury car interior at night. Implemented poorly, it feels like a broken command prompt. Don’t let your brand look broken 50% of the time.
Blinding your users?
We implement flicker-free System Dark Mode that respects battery life and user eyes.