The Dark Mode Trap Everyone Falls Into
I shipped my first dark mode in 2021. I was proud of it. I'd set up CSS custom properties, toggled a class on the <body>, and flipped every color to its rough inverse. White became near-black. Black became near-white. The accent color stayed the same. I pushed it live and called it a day.
Two weeks later, a client reviewed the dashboard I'd built and said something I didn't expect: "The dark version feels harsh. My eyes get tired faster than the light one."
I dismissed it at first. Then I actually sat with the interface for an hour in a dim room. She was right. The contrast was too aggressive. Text that passed WCAG AA technically — 4.5:1 — felt like staring at a fluorescent bulb in a dark hallway. The UI was accessible on paper and exhausting in reality.
That gap between passing a contrast checker and actually feeling comfortable to read is what this post is about.
Why WCAG Ratios Are Necessary But Not Sufficient
Let me be clear: WCAG contrast guidelines exist for a reason and you should follow them. A 4.5:1 ratio for normal text, 3:1 for large text — these are minimums, not targets. They protect users with low vision, colour blindness, and degraded displays.
But here's the part the spec doesn't tell you: the same contrast ratio reads very differently depending on the luminance of your background.
White text on a #121212 background and white text on a #1E1E1E background can both pass 4.5:1, but the first feels clinical and stark while the second feels intentional and calm. The difference is roughly 8 points of lightness in HSL space. That's invisible to a contrast checker. It's not invisible to a human eye spending four hours reading documentation.
Google's Material Design team documented this well when they introduced their dark theme guidelines. Their recommendation was to avoid pure black (#000000) as a surface and instead use a very dark grey around #121212. The reason: in a dark environment, extreme contrast causes halation — the light text appears to bleed or glow into the dark background due to how our pupils respond to high luminance differences. It's not imagined. It's optical physics.
Surface Layering: The Thing Most Dark Modes Skip
In a light UI, you communicate depth through shadows. A card sits above a page, a modal sits above a card. Shadows do that work naturally because they mimic real-world light physics.
In a dark UI, shadows largely disappear. A dark shadow on a dark background is nearly invisible. This is where most dark modes fall apart completely — everything ends up looking flat, and hierarchy collapses.
The solution Material Design, Tailwind's slate palette, and most well-considered dark systems use is elevation through lightness. Surfaces that are higher in the z-axis use a slightly lighter shade of the base dark colour. Not a different colour — a lighter tint.
In practice, this might look like:
- Page background:
#0F1117 - Card surface:
#1A1D27 - Elevated modal or dropdown:
#22263A - Tooltip or highest layer:
#2A2F45
Each step adds roughly 5–8% lightness. The result is a dark UI that still communicates depth without faking it with heavy box shadows. When I rebuilt that client dashboard with this approach, the «harsh» feeling disappeared. The interface still read as dark. It just didn't feel like it was shouting.
Accent Colours in Dark Mode Are Almost Always Wrong
This one stings because I've done it wrong professionally. The assumption is: if the accent colour works in light mode, keep it in dark mode. After all, the brand colour is the brand colour.
The problem is that most brand colours are designed to work on white or light backgrounds. A vibrant blue like #2563EB — Tailwind's blue-600 — looks excellent on white. On a dark surface at #121212, it can still pass contrast but it often reads as dull or slightly muddy. Highly saturated mid-range hues lose punch against dark backgrounds.
The fix is not to change your brand colour. The fix is to use a lighter, slightly less saturated variant for dark mode surfaces. A step or two up the scale — #3B82F6 or even #60A5FA — typically preserves the brand feel while actually feeling alive against a dark background.
I test this now with a simple rule: if I squint at the dark mode button and the accent colour looks like it's receding into the surface rather than sitting on top of it, I go lighter. That's not scientific, but it's caught every mismatched accent in my last six projects.
The Semantic Colour Token Problem
If you're building a proper dark mode with CSS custom properties, you've probably written something like this:
:root {
--color-text-primary: #1a1a1a;
--color-surface: #ffffff;
--color-accent: #2563eb;
}
[data-theme="dark"] {
--color-text-primary: #e2e2e2;
--color-surface: #121212;
--color-accent: #60a5fa;
}
This is the right pattern. But the trap is naming your tokens by their light-mode role rather than their semantic intent. --color-surface is fine. --color-grey-100 as a token name is not — because in dark mode, grey-100 might need to become something that visually looks nothing like grey-100 in light mode.
The token names should describe what the colour does, not what colour it is. --color-text-muted, --color-surface-elevated, --color-border-subtle. When you go to define the dark mode overrides, you're not second-guessing what «grey-100» should translate to — you're asking «what should muted text look like on a dark surface?» which is a much more answerable question.
I refactored three client projects from value-based token names to semantic token names in the past year. Every single one resulted in a dark mode that took half the time to tune correctly.
Dark Mode and Data Visualisation: A Specific Nightmare
Charts, graphs, and status indicators deserve their own mention because they break in ways that text UI doesn't.
The core issue: most chart colour palettes are designed for white backgrounds. On dark surfaces, those same hues often bleed together. Greens and teals that are clearly distinct on white can look almost identical at #121212.
On a dashboard I built for a local logistics company — using Chart.js with vanilla JS — I had a stacked bar chart with five categories. In light mode, the legend was clear and each bar segment was distinct. In dark mode, two of the mid-range colours became nearly indistinguishable. They both passed contrast against the background. They just didn't contrast against each other.
The lesson: in dark mode, you need your data colours to maintain contrast not just against the background, but against each other. Pushing saturation up slightly and spacing hues further apart in the HSL wheel tends to solve this. If your chart library allows separate colour configs per theme — use them. Don't assume one palette serves both.
Practical Checklist Before You Ship a Dark Mode
- Test in an actually dark room. Not just dark mode on a bright monitor at your desk. Get the ambient light low. That's when halation and surface flatness become obvious.
- Check surface layering. Can you tell a card from the page background without a border? You should be able to.
- Audit accent colours independently. Your light-mode accent may need a lighter variant for dark surfaces.
- Test your data visualisation colours against each other, not just against the background.
- Read actual body text for ten minutes. If your eyes are tired, your base contrast is too aggressive or your line length or font weight needs adjustment.
- Check focus indicators. In dark mode, browser default focus rings often disappear entirely. Custom focus styles are not optional.
The Opinion Part
Dark mode has become table stakes. Users expect it. Offering it is no longer a feature — it's a quality signal. A bad dark mode is worse than no dark mode, because it signals that you shipped a checkbox rather than a considered experience.
The contrast ratio checker is a floor, not a ceiling. WCAG compliance tells you the interface won't actively exclude users with visual impairments. It doesn't tell you whether your interface is comfortable, readable, or coherent at 11pm on a phone with adaptive brightness turned on.
The best dark modes I've encountered — Linear, Vercel, Raycast, Obsidian — feel like they were designed in dark mode first. The surfaces have warmth or depth. The accent colours feel alive. The hierarchy is legible without borders propping everything up. That's not magic. It's a separate set of considered decisions, not a variable swap.
If you're going to offer dark mode, design it. Don't toggle it.