Dark Mode Done Right: It’s Not Just Swapping Black and White
In the early days of “Dark Mode” implementation, the approach was simple: take your white background, turn it into pure black (#000000), and turn your black text into pure white (#FFFFFF). It was quick, it was easy, and it was—quite frankly—terrible.
If you’ve ever opened a “lazy” dark mode site in a dark room, you know the feeling. The high contrast of pure white text on a pitch-black background causes “halation”—the text looks like it’s glowing or vibrating, leading to immediate eye strain. As a developer at Nassim Studio, I view dark mode as more than just a toggle; it is a critical part of the user’s sensory experience.
In this post, we’re going to dive deep into how to implement a “High-Fidelity” dark mode. We’ll explore why pure black is a design sin, how to leverage the HSL (and the new OKLCH) color models for perfect harmony, and how to implement this elegantly using Tailwind CSS v4.
The Problem with Binary Contrast
The human eye doesn’t actually see “pure black” or “pure white” in nature. We see gradients of light and shadow. When you use absolute #000000 for your background, you’re creating a “void” where there is no sense of depth or layering.
1. The Halation Effect
Pure white text on a pure black background has a 21:1 contrast ratio. While this sounds good for accessibility, it actually exceeds the “sweet spot” for comfortable reading. The extreme contrast causes the white light from the text to bleed into the surrounding black pixels in your eye, making the characters appear blurry or “fuzzy.”
2. Loss of Elevation
In modern UI design, we use shadows and light to show “elevation” (which items are “closer” to the user). In a pure black interface, shadows are invisible. You lose the ability to show that a modal is floating above the page or that a card is elevated above the background.
The Contrarian Reality: A “Dark Mode” site should feel like a premium library at night—dimly lit but rich in texture and depth—not like a terminal window from 1982.
Mastering HSL: The Secret to Harmony
To build a sophisticated dark mode, you need to move away from the Hex or RGB color models. They are “machine-centric,” not “human-centric.” Instead, we use HSL (Hue, Saturation, Lightness).
HSL allows you to keep your “Hue” consistent across both light and dark modes while only adjusting the “Lightness” and “Saturation.” This ensures that your dark mode still feels like it belongs to your brand.
The “Slightly Saturated” Background
Instead of #000000, I use a very dark version of my brand’s primary hue. If your brand color is a Burnt Orange, your dark mode background should be a very dark “Deep Charcoal” with a 2-3% saturation of that orange. This creates a subtle warmth that feels significantly more premium than a generic gray.
The Elevation Strategy
In dark mode, the “closer” an object is to the user, the lighter its background should be.
* Level 0 (Background): 5% Lightness
* Level 1 (Cards/Sections): 8% Lightness
* Level 2 (Modals/Overlays): 12% Lightness
This creates a sense of depth and hierarchy that mimics how light works in the real world.
Implementation: Tailwind CSS v4 and the OKLCH Revolution
Tailwind CSS v4 has introduced native support for OKLCH, which is an even more advanced color space designed for “Perceptual Brightness.” Unlike HSL, OKLCH ensures that different hues with the same “Lightness” value actually look equally bright to the human eye.
Here is how I structure my “Sovereign” dark mode system using CSS variables and Tailwind.
@theme {
/* Define our base semantic tokens */
--color-bg-base: oklch(15% 0.01 25); /* Very dark, slightly warm charcoal */
--color-bg-surface: oklch(20% 0.01 25); /* Elevated surface */
--color-text-base: oklch(90% 0.01 25); /* Off-white, soft on the eyes */
--color-text-muted: oklch(70% 0.01 25); /* Muted text for secondary info */
/* The magic of dark mode */
@variant dark {
--color-bg-base: oklch(98% 0.01 25); /* Soft white background */
--color-bg-surface: oklch(100% 0 0); /* Pure white surface */
--color-text-base: oklch(15% 0.01 25); /* Dark charcoal text */
}
}
By using these semantic variables, your components don’t need to know if they are in “dark” or “light” mode. They just use bg-bg-base and text-text-base, and the CSS variables handle the rest.
System Preference vs. Manual Toggle
A common debate is whether to force a mode or respect the system preference. The professional answer is: Do both.
- Respect the System: On first load, use the user’s OS-level preference (
prefers-color-scheme). This shows the user that your site respects their environment. - Allow Override: Provide a prominent toggle (usually in the header or footer). When a user clicks it, save their preference to
localStorage. - No Flash of Unstyled Content (FOUC): Use a tiny blocking script in the
<head>of your site to checklocalStorageand apply the correct class before the body renders. This prevents that annoying “white flash” when a dark mode user opens your page.
// In your <head> to prevent flashing
(function() {
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
})();
Accessibility: Beyond the Contrast Ratio
While the WCAG contrast ratio (4.5:1 for normal text) is the standard, dark mode requires extra care.
- Avoid Pure White (#FFFFFF): Use an off-white like
#F9F9F9oroklch(95% ...). It reduces the halation effect significantly. - Decrease Font Weight: Dark text on a light background looks slightly thinner than light text on a dark background. In dark mode, you might want to decrease your
font-weightslightly (e.g., from500to400) to maintain the same visual “weight.” - Saturated Colors in Dark Mode: Be careful with highly saturated colors (like bright blue) on dark backgrounds. They can “vibrate” and cause strain. Desaturate your primary colors by 10-20% when in dark mode for a more balanced look.
Conclusion: Designing for the User’s Context
Dark mode isn’t a feature; it’s a context. It’s a recognition that your user might be reading your technical deep-dives at 2:00 AM in a dimly lit room, or while commuting in a bright train.
By moving away from binary black-and-white swaps and embracing perceptual color models like HSL and OKLCH, you create an interface that feels alive, premium, and—most importantly—comfortable. This level of technical and design foresight is what separates a “commodity” developer from a high-end agency.
What’s your biggest struggle with implementing dark mode? Do you prefer a custom toggle or relying purely on system settings? Let’s discuss in the comments.
Internal Link Suggestion: To see how I handle the typography side of premium design, check out Typography That Sells: Choosing Fonts for a Premium Web Presence.
Leave a comment
Your email address will not be published. Required fields are marked *