Most design-system attempts at dark mode look bad for the same reason. The designer flips the existing light theme, background becomes black, text becomes white, accent colours stay the same, and assumes the job is done. Then users report eye strain, readability complaints, and one specific error message nobody can read because it is pure red on pure black and the vibration between the two is unbearable.
Good dark themes are engineered. They are not inversions. Here is what is actually going on and how to build one that holds up.
Pure black is not the right background
#000000 is only the right background if your display is OLED or MicroLED and you are optimizing for battery savings. On LCD panels, still the majority of screens in 2026, pure black is not actually black. The backlight is always on. #000000 shows up as a slightly-illuminated dark grey, and pure white text on that grey produces the harshest contrast a screen can emit.
This harshness is the single biggest cause of dark-mode eye strain. The human visual system evolved to see light reflected off surfaces, not emitted from them. Maximum-emission pure white on minimum-emission pure black triggers the same fatigue as staring at a bare lightbulb in a dark room.
Good dark themes use a slightly raised black as the base, something in the range of #0B0B0D to #18181B. Dark enough to feel like dark mode. Not so dark that white text becomes a strobe.
The warm/cool tilt matters
A neutral dark grey is mathematically clean but visually unpleasant. Designers have known for decades that pure neutral greys look “clinical” or “plasticky” on screen. Dark themes work better when the background has a slight warm or cool tilt.
Apple’s macOS dark mode tilts cool, with a slight blue undertone. Windows 11 tilts cool too. iOS dark mode tilts almost-neutral but with a hair of blue. Slack and Discord tilt neutral-to-warm.
Why does this help? The human eye is more sensitive to blue in low-light conditions. A cool-tilted background appears “darker” to the eye than a warm-tilted one at the same measured lightness. This matters because it lets you push the background lightness up slightly, giving the user a softer contrast, while still feeling like dark mode.
For most product contexts, tilt slightly cool. Apps that want to feel cozy (reading apps, wellness apps) sometimes tilt slightly warm. Either is fine. Pure neutral grey is the choice that almost always feels wrong.
The ladder
A well-built dark theme is a ladder of background shades, each roughly 2–4% lighter than the last. These are used to communicate depth, cards sit on top of backgrounds, modals on top of cards, popovers on top of modals. Each layer needs its own shade.
A typical ladder for a cool-tilted dark theme might look like:
- Background base,
#0B0B0F, the darkest. Main canvas. - Surface 1,
#15151B, for cards and inline panels. - Surface 2,
#1F1F27, for modals, popovers, floating elements. - Surface 3,
#2A2A33, for nested modals or highest-elevation menus. - Hairline,
#33333D, for border lines and dividers.
Use OKLCH to author these. A ladder in OKLCH is simply a scale of increasing L values at constant C and H. In hex, the same ladder is a set of magic numbers.
This ladder is the skeleton of a good dark theme. Everything else, text, accents, interactive states, sits on top of it.
Text contrast, the 4.5:1 rule and its complications
WCAG 2.1 AA requires a contrast ratio of at least 4.5:1 between text and its background for body text, and 3:1 for large text. This is a minimum, not a target.
On a background of #15151B, these hex values produce the following contrast ratios:
#FFFFFF(pure white), 17.6:1. Comfortably passes. Often too harsh for body text.#E5E5EA(near-white), 14.9:1. Still comfortable. Feels softer, easier to read at length.#A1A1AA(mid grey), 7.3:1. Passes AAA. Good for secondary text.#71717A(darker grey), 4.6:1. Just passes AA. Use for tertiary text, captions.#52525B(dark grey), 3.1:1. Fails AA for body, passes for large text.
Build your text scale at the AAA level (7:1) when possible. The difference between “passes the contrast checker” and “comfortable to read for an hour” is substantial, and most dark themes skew too faint.
Saturation is the silent killer
Here is the trap nobody warns you about. Take a well-designed light-mode accent colour, say, #3366FF, a bright saturated blue. Drop it into dark mode against a #15151B background. It vibrates. The eye cannot settle on the edges. After a few seconds it becomes actively painful.
The problem is not the contrast. The contrast ratio is fine. The problem is that highly saturated colours become overwhelming against dark backgrounds, because the eye has nothing to anchor against. Saturation that reads as “energetic” in light mode reads as “hostile” in dark mode.
The fix is to desaturate and lighten accent colours for dark-mode use. #3366FF becomes something like #6B8EFF, a softer, lighter, less-saturated version of the same hue. The brand colour is still recognizable. The eye strain is gone.
Every mature dark-mode design system has this adjustment built in. Material Design, Apple’s Human Interface Guidelines, Tailwind’s slate/neutral scales, all of them desaturate accents for dark mode.
The “pitch black OLED” variant
One genuine exception to everything above is a pitch-black OLED variant designed specifically for battery savings on OLED phones. On OLED, a truly black pixel is off, consuming no power. A dark theme at #000000 can extend phone battery life by 10–30% compared to a #111111 theme, depending on screen-on time and content mix.
If you ship a mobile app, consider offering a “pitch black” or “AMOLED” variant in addition to your normal dark theme. Build it by taking the normal dark theme and snapping all surfaces to #000000, while keeping the text and accent adjustments from the normal dark theme. Slack, Twitter, and many others offer this.
Do not make it the default. It is uncomfortable on LCD and unnecessary on desktop.
Dark mode is a separate design, not an inversion
The most important thing to internalize is that dark mode is not a CSS filter on your light theme. It is a parallel design system that shares your brand, your type, and your layout, but has its own colour language. Treat it as such. Build the ladder. Build the text scale. Re-tune the accents. Test on LCD and OLED. Proof on a real device, not a simulator.
This is more work than one prefers-color-scheme media query. It is also the difference between a dark mode that people enable and use, and a dark mode that appears as a checkbox in the settings and gets ignored.
If you want to see what this looks like done well, look at the macOS system UI, Linear, or Stripe’s dashboard. If you want to see it done poorly, look at the majority of indie apps that inverted their light theme and shipped. Your eyes already know the difference.
One more thing
Test dark mode with real content, in a real dark room, at 11pm. The subtle problems only show up at the edges of fatigue. A theme that looks perfect in the office at noon is not a theme that has been tested.