Good dark mode design is far more than inverting the colors of a light theme. Making the background black and the text white seems to work at first glance, but in real life pure white on pure black creates a tiring glow (halation), shadows disappear, and your brand colors turn harsh. A dark theme is a separate design system that needs to be rethought with its own logic, its own gray palette and its own contrast rules. In this post I'll walk through the right gray tones, readable contrast ratios and color choices that reduce eye strain, with practical examples.
Why pure black (#000000) is a poor starting point
Most people's first instinct is to set the background to #000000, but this causes several problems. Pure white text on pure black produces extreme contrast; especially on OLED screens the edges of bright letters smear and the eye tires during long reading sessions. On a pure black surface a drop shadow also becomes invisible, so you lose one of the most powerful tools you have for separating layers (cards, modals, menus) from one another.
Use a very dark gray instead. Google's Material design guidance has for years recommended roughly #121212 as the base surface color, because on that tone you can stack semi-transparent white layers to create a sense of elevation. A practical starting palette might look like this:
--bg: #121212; /* lowest surface */
--surface-1: #1e1e1e; /* cards */
--surface-2: #242424; /* raised cards, menus */
--surface-3: #2c2c2c; /* modals, popovers */
--text: #e6e6e6; /* primary text (not pure white) */
--text-dim: #a0a0a0; /* secondary text */
Notice how the surface color gets lighter as the layer rises. In a dark theme, because light comes from above, higher surfaces appear brighter; this mimics real-world lighting logic and makes the interface feel natural.
Softened white instead of pure white for text
Don't make body text #ffffff. On a dark surface, pure white creates a vibration effect around the letters. Use white at 85–90% opacity or a tone around #e6e6e6 instead. The Material guidelines scale text opacities like this:
- High emphasis (body text): 87% of white
- Medium emphasis (secondary text, labels): 60% of white
- Disabled (inactive elements): 38% of white
In CSS it's more predictable to write this as a direct color rather than opacity, because if you make white transparent with rgba the surface color underneath changes the result. You can use either, but stay consistent within a project.
Contrast ratio: respect the WCAG limits
The most common mistake in dark mode design is lowering contrast for the sake of aesthetics. Faded gray text on a dark surface may look "sleek" but it's unreadable. The WCAG 2.1 accessibility standard sets clear thresholds:
- Normal text: at least a 4.5:1 contrast ratio
- Large text (above 18pt / 24px, or 14pt bold): at least 3:1
- UI components (button borders, icons, form field outlines): at least 3:1
An example: on a #121212 background, #e6e6e6 text has a contrast ratio of roughly 13:1, which is more than enough. But if you use #757575 for secondary text the ratio drops to about 4.6:1, which barely passes. Pick #5a5a5a and it falls below 3:1 and cannot be used for normal text. Verify every color pair with your browser's dev-tools contrast checker or a tool like the WebAIM Contrast Checker.
Desaturating vivid colors
Bright, saturated colors that look great in a light theme (vivid blue, red, green) "float" and vibrate against a dark surface. This happens because the large luminance gap between high saturation and a dark background strains the eye. The fix: lower the saturation and slightly raise the lightness for dark mode. In other words, use more pastel, more "dusty" tones.
/* Accent color in the light theme */
--accent-light: #0066ff; /* vivid blue */
/* Softened version for dark mode */
--accent-dark: #5b9aff; /* lighter, less saturated */
The same rule applies to brand colors. If your primary brand color is very dark (a navy blue, say), it will blend into the surface and become unreadable in dark mode; in that case you need to derive a lighter variant for accessibility. Keeping color tokens theme-based (a light and a dark value for each color) makes this manageable.
Elevation, shadows and borders
In a light theme you separate layers with shadows. In a dark theme shadows work poorly because the background is already dark. Instead, combine two techniques: a lighter surface color (the layer scale above) and a subtle border. A very low-opacity white border separates a card from the surface gracefully:
.card {
background: var(--surface-1);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
}
Don't drop shadows entirely; just make them softer and more diffuse, so they suggest depth rather than hardness. In a dark theme the goal isn't a dramatic shadow but a gentle separation.
Images and the theme transition
Photos and illustrations can stay too bright on a dark surface; a very slight dimming (filter: brightness(0.9)) or a thin border on content images improves visual comfort. Transparent PNG logos, especially dark-colored ones, disappear in dark mode, so prepare a light variant of your logo.
Finally, handle the transition between themes. Remember the user's choice (for example in localStorage) and read the system preference with prefers-color-scheme as the initial value. To prevent the transition from "flashing," apply the theme class before the page first renders, ideally with a tiny script inside <head>.
Frequently Asked Questions
Does dark mode really tire the eyes less?
It depends on context. In a dark environment a dark theme is easier on the eyes because the screen's overall brightness drops. But in a bright environment a dark theme can dilate the pupil and make text edges appear blurrier. The best approach is to give users a choice and respect their system preference.
Do I need a separate color palette for dark mode?
Yes, at least for accent and text colors. Carrying the light theme's colors over unchanged creates contrast and saturation problems. Defining color tokens with separate values per theme is the most sustainable approach.
Should I use pure black for OLED screens?
For battery savings pure black does turn pixels off, that's true. But making the entire interface #000000 breaks readability and layer separation. A balanced approach: keep the main surface very dark like #121212 and use true black only where you specifically want it.
Want an interface that works flawlessly in both light and dark themes for your brand? I can help design accessible contrast, a consistent color system and an eye-friendly dark mode. Get in touch and let's talk about your project.