On this page
Why inversion fails
The naive dark mode — swap white for black, flip every color to its opposite lightness — fails for a reason that's worth understanding: light mode palettes encode a set of assumptions that stop being true in the dark. Shadows communicate elevation; saturated brand colors sit comfortably on white; pure black text is the contrast king. Invert the canvas and every one of those assumptions breaks in its own way. Good dark modes aren't inverted — they're redesigned under different physics.
Don't use pure black
Almost every mature dark theme uses very dark gray, not #000000 — Material Design's famous baseline is #121212. The reasons are practical. Pure white text on pure black is the maximum possible contrast (21:1), and at typical body-text sizes that extreme causes a visual smearing effect — halation — that many users, particularly those with astigmatism, find genuinely harder to read than slightly softened contrast. Near-black also leaves headroom below your surfaces: if the base is #121212, hover states and pressed states can go darker; from #000000 there's nowhere to go. And dimmed text colors that read fine on dark gray tend to vanish on true black.
The same restraint applies to text: prefer near-white (around #e5e5e5–#f5f5f5) over #ffffff for body copy, reserving pure white for small high-emphasis moments.
Elevation: light replaces shadow
On white, hierarchy comes from shadows — a card floats because it casts one. On a dark canvas shadows are nearly invisible, so elevation flips its encoding: higher surfaces get lighter. A modal sits on a lighter gray than the card below it, which is lighter than the page background. Practically, that means dark mode needs a ladder of dark grays — something like #0a0a0a → #171717 → #262626 → #404040 (the dark end of the neutral scale in our color chart) — where light mode got away with white plus shadows. If your dark UI looks flat, this is almost always what's missing.
Desaturate what sits on dark
Fully saturated colors that look confident on white tend to "vibrate" against dark backgrounds — optically buzzy, with colored halos at the edges — and saturated darks (a 700-level brand blue, say) can fail contrast against a dark surface entirely. The standard move is to shift accent colors toward lighter, less saturated variants in dark mode: where light mode used your brand's 600, dark mode probably wants its 300 or 400 — a tint doing structural work. This is also the cleanest way to keep brand identity: hold the hue, move lightness and saturation. Generate your color's lighter variants with the palette generator and test each against your actual dark surfaces, not against black.
Contrast math doesn't get a night off
WCAG thresholds are identical in the dark: 4.5:1 for body text, 3:1 for large text and UI parts. Two traps are specific to dark mode. First, mid-gray secondary text that passed on white often fails on #171717 — check the real pairs in the contrast checker rather than porting the light-mode grays. Second, contrast that's technically passing can still be uncomfortable at the extremes, per the halation problem above — dark mode is the one place where you're occasionally softening contrast on purpose, and that makes deliberate measurement more important, not less.
Images, and the details that give it away
Full-strength photography and illustration can glare against dark surfaces; many systems dim media slightly (90–95% opacity or a subtle dark overlay) so it sits in the page rather than punching through it. Semantic colors — success, warning, error — need the same lighter-variant treatment as brand accents, re-verified for contrast on dark. And borders often work better lighter than surfaces in dark mode (rgba(255 255 255 / 0.1)-style washes), where light mode used darker-than-surface lines.
Implementation notes that save rework
Respect the user's setting via the prefers-color-scheme media query, and design tokens semantically from day one: a token named surface or text-secondary can hold different values per scheme; a token named gray-100 cannot. If you're building the scale programmatically, do the lightness math in a perceptual space like OKLCH so "three steps lighter" looks like three even steps on both ends of the ladder. Then audit the dark theme as its own first-class deliverable — every pair, both schemes — because half of your users may see it more hours per day than the light one.