Dark Mode Is Not Just an Invert Filter: The Color Contrast Decisions That Actually Matter
01The Dark Mode Trap Everyone Falls Into
The quick way to ship a dark mode goes like this: set up CSS custom properties, toggle a class on the <body>, and flip every color to its rough inverse. White becomes near-black. Black becomes near-white. The accent color stays the same. Push it live and call it a day.
The result often feels harsh. Spend an hour with an interface like that in a dim room and the problem is obvious: the contrast is too aggressive. Text that technically passes WCAG AA — 4.5:1 — can feel like staring at a fluorescent bulb in a dark hallway. The UI is 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.
02Why WCAG Ratios Are Necessary But Not Sufficient
To 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 both pass 4.5:1, but the first feels clinical and stark while the second feels intentional and calm. That difference is 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.
03Surface 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 a few points of lightness. The result is a dark UI that still communicates depth without faking it with heavy box shadows. The interface still reads as dark. It just doesn't feel like it's shouting.
04Accent Colours in Dark Mode Are Almost Always Wrong
The common 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.
A simple test: squint at the dark mode button. If the accent colour looks like it's receding into the surface rather than sitting on top of it, go lighter. That's not scientific, but it catches mismatched accents quickly.
05The 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.
That's why semantic token names make a dark mode much faster to tune than value-based ones.
06Dark 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.
Picture a dashboard with a stacked bar chart of five categories. In light mode, the legend is clear and each bar segment is distinct. In dark mode, two of the mid-range colours become nearly indistinguishable. They both pass contrast against the background. They just don'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.
07Practical 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.
08The Bigger Picture
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.
Linear, Vercel, Raycast and Obsidian are often cited as strong dark modes. They 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.
Denny Pratama
Product designer in Indonesia. Since 2018 I’ve designed SaaS products, dashboards and marketing sites, and now I build my own designs too.