We use cookies
To improve your experience. Cookie policy
Design
Dark mode is easier to add at the start than to retrofit — and the decisions you make in your token system determine whether it feels considered or patchy.
Léa Fontaine
Design Lead
Dark mode is the single most-requested feature in modern application design. It's also the feature most commonly implemented as an afterthought — a filter: invert() hack or a handful of hard-coded overrides that make the dark version feel like a different product entirely.
Done well, dark mode reveals the quality of your design token system. Done poorly, it exposes the lack of one.
The mistake is mapping utilities directly to palette values: bg-zinc-900 dark:bg-zinc-50. The moment you do this, every new dark mode component is a hunt-and-replace problem.
The right model: define semantic tokens as CSS custom properties, map your Tailwind utilities to those tokens, and change the tokens per theme. With Tailwind v4's @theme inline and CSS variable system:
:root { --bg-base: #ffffff; --text-primary: #09090b; }
.dark { --bg-base: #0a0a0a; --text-primary: #fafafa; }
@theme inline { --color-bg-base: var(--bg-base); }
Now bg-bg-base works in both themes without any dark: prefix. Your components stay clean.
Light mode colours don't invert cleanly. A zinc-900 on white reads at contrast ratio 19:1 — dramatically over-engineered for body copy. In dark mode, zinc-50 on zinc-950 is also fine, but the relationship between foreground and background saturations needs manual tuning to feel right.
A practical approach: build your dark palette independently, targeting WCAG AA (4.5:1 for body text, 3:1 for large text and UI components) rather than copying inverted light-mode values.
Your brand accent (say, #7C5CFF) might pass contrast on white but fail on your dark background. Always test both. A common fix is a slightly lighter accent for dark mode — #8F72FF maintains the hue but gains enough brightness to hit 4.5:1 against a near-black surface.
prefers-color-scheme is a user's statement about their environment, accessibility needs, or personal preference. Default to it. Offer an override in settings. Persist the override to localStorage. This is the pattern every mature product has settled on — and it's what we implement in every project.

Written by
Léa Fontaine
Design Lead
Léa leads product design at stackloader, with a focus on design systems, accessibility, and the intersection of engineering and user experience. She believes good design is invisible — and dark mode is a test of whether your token architecture is real or decorative.
More from the blog
Newsletter
New articles on AI, DevOps, and engineering craft. Roughly twice a month. No noise, no promotions — just the good stuff.