RU Send your project
All articles

Dark mode without the pain

Layered surfaces instead of pure black, text contrast, separate accent and status colors, and the tokens without which dark mode can't be maintained.

· 4 min readDesignCode

Dark mode often gets built in an evening: background to black, text to white, done. A week later it turns out the cards blend into the background, the shadows are gone, the gray text is unreadable, and half the components still have light patches because their colors were hardcoded.

Dark mode isn’t a repaint. It’s a second palette that follows the same logic. Doing it properly isn’t hard if you start from the foundation.

Not pure black

Pure white text on a pure black background gives maximum contrast, and that’s the problem: the text glares, and some screens smear when you scroll. More importantly, pure black leaves you no room to show depth.

In a light theme, raised elements are separated by shadows. In a dark theme shadows are nearly invisible, so depth comes from lightness: the higher the layer, the lighter it is. Three or four levels usually do it:

  • page background, the darkest, but not black;
  • surface, for cards and panels;
  • raised surface, for menus and popovers;
  • hover and selection.

The steps between levels are small but noticeable. A thin border helps a lot too: a one-pixel light line at low opacity separates surfaces better than any shadow.

Text and contrast

On a dark background, tone white text down slightly to a light gray. Secondary text, on the other hand, can’t go too dark. What read as a calm gray on white easily drops below acceptable contrast on dark. Check contrast in each theme separately instead of carrying values over.

Thin weights look even thinner on dark backgrounds. If your body text uses a light weight, it may need to go one step heavier in dark mode.

Accent and status colors

Saturated colors picked for a white background start to glow and glare on a dark one. For dark mode, accent and status colors are usually made lighter and slightly less saturated. Pick the error red, success green and warning yellow separately, and check that they’re distinguishable from each other and from the background.

Watch the text on accent buttons. If the accent turns light in dark mode, the text on it needs to turn dark. A white icon on a white button is a classic.

Tokens

None of this is maintainable if colors are scattered through the code. The foundation of dark mode is tokens named by role: background, surface, text, muted text, border, accent. Components use only those, and themes override the values.

:root {
  --bg: #ffffff;
  --surface: #f5f5f6;
  --text: #16161a;
  --text-muted: #63636b;
  --border: rgb(0 0 0 / 0.08);
}
[data-theme="dark"] {
  --bg: #0f0f11;
  --surface: #17171a;
  --text: #e8e8ea;
  --text-muted: #9a9aa2;
  --border: rgb(255 255 255 / 0.08);
}

With that in place, every new screen gets dark mode for free, and searching the code for hardcoded colors shows what’s left to convert.

Images, charts and small things

Logos and illustrations on transparent backgrounds can vanish in dark mode: a dark logo on a dark background simply isn’t there. They need their own variants. Images with white backgrounds look like patches, so cut the background out.

Charts need their own grid and label colors. Inputs, scrollbars and other native controls need the right color-scheme property, or the browser will draw them light in the middle of a dark page.

Choosing the theme

Follow the system setting by default and offer a three-way switch: light, dark, system. Remember the choice and apply it before the page renders, or the screen will flash white on every load.

SUBXCAT moves interfaces onto tokens and gets both themes to the point where they don’t need fixing on every new screen.

Want us to do this for you?

Tell our assistant about your project — a rough estimate in five minutes, a review in 48 hours.

Book an intro call 20 minutes: an engineer, a designer and a marketer look at your project with you.
Book a call