How to create a dark mode palette that actually works

March 2026 · 6 min read

Most dark mode implementations are bad. Not "needs polish" bad. Fundamentally broken. Colors that passed contrast checks on light mode now fail on dark. Accents that popped against cream now vanish against charcoal. Surfaces that felt layered now feel flat.

The root cause is always the same: someone took their light palette and inverted it. Swapped the background from light to dark, flipped the text from dark to light, and called it done. That's not dark mode. That's a color inversion filter with extra steps.

Real dark mode requires a second palette. Derived from your light palette, yes. But adjusted for how the human eye perceives color on dark backgrounds. Here's how to do it right.

Why inversion fails

Your eye processes light and dark backgrounds differently. On a light background, dark text at 4.5:1 contrast ratio looks perfectly readable. On a dark background, light text at the same 4.5:1 ratio can feel harsh — especially at small sizes. Pure white on pure black is technically high contrast, but it causes eye strain. The text glows.

Three things break when you invert:

  • Perceived contrast shifts. The same contrast ratio feels different on dark versus light surfaces. Dark mode usually needs slightly lower contrast for body text to feel comfortable — think #E0E0E0 on #1A1A1A rather than #FFFFFF on #000000.
  • Saturation amplifies. Saturated colors that look balanced on light backgrounds become overwhelming on dark backgrounds. That warm gold accent that was pleasant on cream becomes a neon beacon on charcoal. You need to desaturate or shift the hue.
  • Surface hierarchy flattens. In light mode, you layer surfaces with subtle shade differences: white, off-white, light gray. In dark mode, you need to go the other direction — not darker, but slightly lighter. Elevated surfaces in dark mode are lighter than the base, not darker. This breaks most people's intuition.

The role-based approach

If your palette uses role-based naming, dark mode becomes systematic instead of guesswork. Each role has a clear transformation rule.

Background: goes dark

Your light background (#FAF7F2) becomes a dark surface. Not pure black — too harsh. A warm dark gray that maintains the same hue character as your light background. If your light mode feels warm, your dark mode should feel warm too. Something like #1A1917 — charcoal with a hint of warmth.

Ink: goes light

Your dark ink (#1A1A1A) becomes light text. But not pure white. Off-white reduces eye strain and feels more refined. #F0EDE8 instead of #FFFFFF. Match the warm/cool character of your light-mode ink.

Accent: stays, but adjusts

This is the tricky one. Your accent color needs to remain recognizable — it's your brand. But it may need luminance adjustment to maintain contrast against the new dark background. A gold accent (#E8C547) might need to shift slightly lighter (#F0D060) to maintain the same visual weight. Or it might need desaturation to avoid the neon effect.

Support: brightens

Support text (#7A7A6E in light mode) needs to be lighter in dark mode, but still clearly secondary to ink. It occupies the middle ground. Something like #9A9A8E — visible but recessive.

Neutral: darkens slightly

Neutral colors handle borders and dividers. In dark mode, they need to be visible against the dark background without being too prominent. A very subtle light-dark value: #2A2A28. Just enough to create structure.

Step by step: light to dark

Here's the practical workflow. Start with your light palette. Derive the dark variant. Export both as CSS custom properties.

1. Generate your light palette

Create your palette on Paletter. Get your five roles assigned. This is your canonical palette — the one that defines your brand.

2. Derive the dark variant

For each role, apply the transformation rules above. You're not picking new colors — you're translating your existing palette for dark surfaces. Keep the hue relationships. Adjust lightness and saturation.

3. Export as CSS custom properties

Define your light palette as the default. Add the dark palette inside a prefers-color-scheme media query. Same variable names, different values.

/* Light palette (default) */
:root {
  --color-background: #FAF7F2;
  --color-ink: #1A1A1A;
  --color-accent: #E8C547;
  --color-support: #7A7A6E;
  --color-neutral: #D4CFC6;
}

/* Dark palette */
@media (prefers-color-scheme: dark) {
  :root {
    --color-background: #1A1917;
    --color-ink: #F0EDE8;
    --color-accent: #F0D060;
    --color-support: #9A9A8E;
    --color-neutral: #2A2A28;
  }
}

Every component that uses var(--color-background) or var(--color-ink) automatically adapts. No class toggling. No JavaScript. The cascade handles everything.

Manual toggle pattern

If you want a toggle button instead of (or in addition to) system preference, use a data attribute on the root element.

/* System preference */
@media (prefers-color-scheme: dark) {
  :root {
    --color-background: #1A1917;
    --color-ink: #F0EDE8;
    /* ... */
  }
}

/* Manual override */
:root[data-theme="dark"] {
  --color-background: #1A1917;
  --color-ink: #F0EDE8;
  /* ... */
}

:root[data-theme="light"] {
  --color-background: #FAF7F2;
  --color-ink: #1A1A1A;
  /* ... */
}

Then toggle with one line of JavaScript: document.documentElement.dataset.theme = 'dark'. Store the preference in localStorage and apply it on page load before first paint to avoid the flash.

WCAG considerations

Contrast ratios that pass in light mode may fail in dark mode. This catches people off guard. You have to re-check every color pair.

  • Body text: Needs 4.5:1 contrast ratio (WCAG AA). Your dark mode ink on your dark mode background must hit this. Pure off-white on dark gray usually passes. But check it.
  • Large text: Needs 3:1 (WCAG AA). Headings and display text have more room. This is where your accent color on a dark background matters — if it's a heading color, 3:1 is the bar.
  • Interactive elements: Buttons, links, focus rings. Your accent must be distinguishable against the dark background. If your light-mode accent was subtle, your dark-mode accent might need to be bolder.
  • Support text: This is the one that usually fails. Support text is intentionally lower contrast. In light mode, a 5:1 ratio feels subdued. In dark mode, a 5:1 ratio might actually be too low because of how the eye perceives light text. Test at actual body text sizes, not just in a contrast checker.

The takeaway: don't assume. Run your dark palette through a contrast checker with every meaningful color pair. Paletter's export includes pre-computed contrast ratios for both light and dark variants, so you see pass/fail immediately.

The common pitfalls

  • Pure black backgrounds. #000000 is too stark. Use a dark gray with warmth or coolness that matches your brand personality. #1A1A1A is a safe neutral starting point.
  • Pure white text. #FFFFFF on dark backgrounds causes halation — the text appears to bleed into the background. Use off-white. #F0F0F0 or warmer.
  • Same shadows. Light mode shadows are dark with transparency. Dark mode shadows need to be darker or replaced with subtle borders or elevation changes using lighter surfaces.
  • Forgetting images. Full-bleed images designed for light backgrounds can be jarring in dark mode. Consider subtle border treatments or reduced brightness for decorative images.

Start with light. Derive dark.

Don't design your dark palette from scratch. Generate your light palette first — that's your brand. Then derive the dark variant using the role-based rules above. Same hue relationships. Adjusted lightness and saturation. Re-checked contrast.

Two palettes, one system. CSS custom properties make the swap effortless. Your components don't know which mode they're in. They don't need to.

Start with a light palette. We'll help with the rest.

Generate a curated five-color palette with role assignments and contrast checks. Export as CSS custom properties and build your dark variant on a solid foundation.

Generate your palette