The first dark theme I shipped was, in hindsight, a disaster. I took our light UI, ran every color through a "invert" filter, nudged a few values by hand, and called it a night. It looked fine in Figma. Then the support tickets came in: text that "buzzed," brand buttons that glowed like radioactive candy, cards that all blended into one flat sheet of darkness. The lesson stuck. A good dark mode color palette is not a light palette turned inside out. It is its own design problem with its own rules, and most of those rules push against your instincts.
Here is what I wish someone had told me before that first attempt.
Dark mode is not an inversion
When you invert a light theme, every relationship that made it work gets scrambled. White text on a navy button might be perfect at 8:1 contrast in light mode; flip it and you could land on a pale button with gray text that fails accessibility entirely. Shadows, which carry a lot of the depth in light UIs, basically vanish on a dark background — a 10% black shadow over a near-black surface is invisible. Saturated colors that read as confident and friendly on white become aggressive and vibrating on black.
So the mental model I use is this: a dark theme is a separate set of design decisions that happens to share the same layout and the same brand. You are re-solving contrast, hierarchy, depth, and accent color from scratch, against a dark canvas. If your tooling lets you maintain two color sets (semantic tokens are the clean way to do this — more on that in our piece on design system color tokens), this is the moment it pays off.
Use dark gray, not pure black
This is the single most common mistake, and it is worth understanding why pure black fails rather than just being told to avoid it.
Pure black (#000000) paired with pure white (#FFFFFF) text technically scores 21:1 — the maximum possible contrast ratio. You would think maximum contrast is the goal. It is not. That extreme luminance gap causes halation: on a dark background the pupil dilates to let in more light, and bright text bleeds and smears at the edges. For people with astigmatism — a large slice of the population — the effect turns paragraphs into a glowing, fatiguing mush. Long reading sessions become genuinely uncomfortable.
There is a second, more practical problem. If your base layer is #000000, you have nowhere to go down. You cannot make a surface darker than black, so any sense of recessed depth is gone, and shadows have nothing to fall onto.
The fix is to start from a very dark gray. Google's Material Design recommends #121212 as the baseline dark surface, and after years of building these, I think it is close to perfect. It is dark enough to read as "dark mode," but it leaves headroom in both directions. For text, drop pure white in favor of an off-white in the #E0E0E0 to #F2F2F2 range. White-on-#121212 at that level still clears WCAG comfortably while killing the halation glow.
A neutral starting palette I reach for often:
- Base background: #121212
- Primary text: #E6E6E6 (high emphasis)
- Secondary text: #A8A8A8 (medium emphasis, still well above 4.5:1)
- Disabled / hint text: around #6E6E6E
One subtle trick: tint your dark grays very slightly toward your brand hue instead of using pure neutral grays. A base of #131316 with a hair of blue, or #14110F with a touch of warmth, feels intentional and branded rather than like a default theme. Keep it subtle — a few points of saturation at most.
Convey elevation with lighter surfaces, not shadows
In light mode, a card floats above the page because of its shadow. In dark mode, shadows barely register, so we flip the logic: the higher a surface sits, the lighter it gets. A modal is lighter than the card it covers, which is lighter than the page behind it.
Material formalizes this as an elevation overlay — a semi-transparent white layer applied on top of the base surface, getting more opaque as elevation increases (roughly 0% at the base up to about 16% at the highest level). You do not have to implement the literal overlay math; you can just bake the results into discrete tokens. A scale I have shipped and trust:
- Level 0 (page): #121212
- Level 1 (cards): #1E1E1E
- Level 2 (menus, raised panels): #242424
- Level 3 (dialogs, modals): #2C2C2C
- Level 4 (popovers, top sheets): #333333
Each step is only a handful of points lighter, and that restraint matters — the jumps should be felt, not announced. Used consistently, this gives your interface real depth without a single shadow, and it solves the "everything blends together" complaint that plagued my first attempt. You can still layer a faint shadow on top for extra separation, but the lightness shift is what does the heavy lifting.
Desaturate your accent colors
Take your brand's hero color — say a punchy SaaS blue like #2563EB — and drop it onto a #121212 background. It vibrates. Highly saturated colors against a dark field produce an optical buzz, and they often fail contrast for text or icons even though they look loud.
The fix is counterintuitive: pull saturation down and lightness up. Material's guidance is to use lighter tints (think the 200–50 range of a color scale) and desaturate them until they pass 4.5:1 against the dark surface. In practice I desaturate brand colors by roughly 20–40% for dark mode and lighten them, then check the number.
Concretely, that #2563EB might become something like #7AA2F7 in dark mode — softer, lighter, and far more legible against the dark surface. Your light-mode accent and your dark-mode accent are siblings, not the same value. This is exactly the kind of paired light/dark exploration the color palette generator is built for: pick your hue, then nudge saturation and lightness while watching the contrast number update live.
A few honest caveats:
- Keep semantic colors recognizable. Error red, success green, and warning amber still need to read as red, green, and amber after you desaturate them. Lighten and soften, but do not drift the hue so far that an error stops looking like danger.
- Large color fills are different from text. A big saturated hero banner can survive more saturation than a small label, because you are not asking anyone to read fine text off it.
- Don't apply elevation overlays to colored surfaces. A primary-colored button keeps its color across elevation levels; only your neutral surfaces lighten as they rise.
Keep WCAG contrast — in both directions
The accessibility targets do not change in the dark. WCAG 2.1 requires 4.5:1 for normal body text and 3:1 for large text (and for meaningful UI components and icons). What changes is that the failures are sneakier.
The trap I see most often is mid-tone gray text — something like #888888 on #121212 — that looks elegant and quietly fails for secondary copy. Always run the actual numbers. The official reference is the W3C WCAG contrast guidance, and any contrast checker (including the one built into our color palette generator) will give you the ratio in seconds.
Two things worth knowing as you test. First, "more contrast is always better" is false in dark mode — pure white maximizes the number but triggers the halation I described, so you are optimizing for comfortable legibility, not the highest possible ratio. Second, the next generation of the standard, WCAG 3.0, replaces the simple ratio with APCA (the Advanced Perceptual Contrast Algorithm), which models how the eye actually perceives light and dark text and handles polarity far better. It is still in draft, so ship against WCAG 2.1 today, but if you want to future-proof, run your dark palette through an APCA checker as a sanity test — it is noticeably more honest about dark backgrounds.
Test it where people actually use it
A dark palette can pass every check in Figma and still fail in the wild. The variable I most underestimated early on was the screen itself: OLED panels render #000000 as truly off pixels with brutal contrast against lit text, while cheaper LCDs lift the blacks and wash out your carefully tuned elevation steps. The palette that looks layered on your monitor can look flat on someone's budget laptop.
My testing checklist, learned the hard way:
- View on a real OLED phone at low brightness, at night. This is where halation and over-saturation reveal themselves. If it is comfortable here, it is comfortable everywhere.
- Check the elevation steps on a cheap LCD. If your cards disappear into the background, your steps are too subtle.
- Toggle between light and dark live. Jarring brightness flashes usually mean your dark base is too light or your light base too gray.
- Look at it with images and user content. Real photos, avatars, and embedded media sit on your surfaces and can clash in ways a clean mockup never shows.
- Confirm focus states and borders survive. Subtle 1px borders that defined inputs in light mode often vanish in the dark and need a lighter value.
The short version
If I had to compress everything into a handful of moves: start from #121212, never #000000. Use off-white text, not pure white. Show depth by lightening raised surfaces, since shadows barely exist here. Desaturate and lighten your accents until they stop buzzing and start passing contrast. Hold the line at 4.5:1 for body text, but resist the urge to chase the maximum. And test on a real OLED screen in a dark room, because that is the one environment your mockups can never simulate.
Done right, a dark theme is not just a polite option for night owls — it is a calmer, more focused version of your product. It just takes treating it as the distinct design problem it actually is.
Frequently Asked Questions
Why shouldn't I use pure black (#000000) for dark mode backgrounds?
Pure black paired with white text hits 21:1 contrast, but that extreme gap causes halation — bright text bleeds and glows at the edges, which is especially fatiguing for people with astigmatism. Pure black also gives you no room to show recessed depth, since nothing can be darker than black. Start from a very dark gray like #121212 instead, which reads as 'dark' while leaving headroom for elevation and comfortable text contrast.
How do you show elevation or depth in a dark mode UI?
Shadows barely register on dark backgrounds, so the convention is reversed: the higher a surface sits, the lighter it gets. The page might be #121212, cards #1E1E1E, menus #242424, and dialogs #2C2C2C. Each step is only a few points lighter. This 'elevation overlay' approach (a semi-transparent white layer that grows with height) gives real depth without relying on shadows.
How much should I desaturate brand colors for dark mode?
Roughly 20–40%, while also lightening them. A saturated brand blue like #2563EB vibrates against a dark surface and can fail contrast; softened and lightened to something like #7AA2F7, it stays legible and calm. Use lighter tints (the 50–200 range of a color scale) and verify each accent clears 4.5:1 against your dark surface. Keep semantic colors like error red recognizable — soften the saturation, not the hue.
What contrast ratio does text need in dark mode?
The WCAG 2.1 targets don't change: 4.5:1 for normal body text and 3:1 for large text and meaningful UI components. The catch is that elegant mid-gray text (like #888888 on #121212) often fails quietly, so always check the actual number. Note that 'more contrast is better' is false here — pure white maximizes the ratio but triggers halation, so aim for comfortable legibility rather than the absolute highest value.
Why does my dark theme look different on different phones?
Screen technology matters more than people expect. OLED panels render #000000 as fully off pixels with harsh contrast, while cheaper LCDs lift the blacks and wash out subtle elevation steps. A palette that looks nicely layered on your monitor can look flat on a budget screen. Always test on a real OLED phone at low brightness in a dark room, and also on an inexpensive LCD, before shipping.
Want to experiment with colors?
Try our free color palette generator to find your perfect harmony — with a built-in WCAG contrast checker.
Open the Generator