Dark mode isn't just flipping a colour palette. When I started building Spectral, I assumed it'd be a two-hour task. Six weeks later, I had a completely rethought approach to colour, contrast, and depth that changed the way I design for Android.
The naive approach (and why it fails)
My first attempt was exactly what you'd expect: take every light colour, invert it, done. White backgrounds became near-black. Dark text became near-white. I shipped it to a handful of beta users and within 48 hours the feedback was unanimous - it looked harsh, clinical, and nothing like the premium feel I was going for.
The problem is that human perception isn't linear. A white card on a light grey background creates a subtle sense of elevation. Invert that and you get a black card on a slightly-lighter-black background - and the depth completely disappears.
Elevation through lightness, not shadow
In Material 3's dark theme guidelines, Google documents something counter-intuitive: in dark mode, elevated surfaces should be lighter, not darker. A card that floats above the background should use a slightly higher lightness value - maybe #1e1e2e against a base of #12121f.
This mirrors how light actually works. In a dark room, objects closer to a light source pick up more of that light. Depth means brightness, not shadow.
For Spectral I ended up with a four-level elevation system:
- Level 0 (background):
#0d0d1a - Level 1 (cards):
#161626 - Level 2 (modals):
#1e1e32 - Level 3 (menus):
#26263e
The differences are subtle. That's the point. If a user consciously notices the layers, the gradation is too aggressive.
Colour saturation in dark contexts
The purple accent I use throughout Spectral was born in a light-mode prototype. Dropped into a dark background, it screamed. The solution isn't to desaturate - that kills vibrancy - it's to reduce chroma while preserving hue.
Working in the OKLCH colour space made this straightforward. Instead of adjusting HSL sliders and eyeballing the result, I could dial the C (chroma) value down incrementally while keeping L and H fixed. The result: a purple that still reads as purple, but doesn't assault the eyes at 2am.
Text contrast: the 4.5:1 trap
WCAG AA requires 4.5:1 contrast for normal text. This is a floor, not a target. On a dark background, pure white at 21:1 contrast is technically correct and visually exhausting. I settled on three text levels:
- Primary: 92% opacity - headers, labels
- Secondary: 65% - body copy, descriptions
- Tertiary: 38% - placeholders, disabled states
The takeaway
Dark mode is a first-class design problem, not an afterthought. The apps that get it right share one thing in common: their dark mode looks like it was designed in the dark, from scratch. Not like a light mode that got inverted.
For Spectral, the six weeks I spent on this turned out to be some of the highest-leverage design work in the project. Users don't comment on the elevation system or the OKLCH adjustments. They just say it feels polished. That's the goal.