Your Light Palette Doesn't Invert to Dark Mode
#f8fafc does not become #070509. That's the first thing to understand about dark mode.
Color inversion gives you technically inverted values that violate contrast requirements, blow out saturated accents, and produce surfaces that strain vision. Dark mode requires a separate palette — same three roles (dominant, secondary, accent), different hex values optimized for dark surfaces.
The 60-30-10 proportions stay the same. The dominant color still covers 60% of the surface area. The secondary still handles structural elements. The accent still appears at 10% for CTAs and active states. What changes is what "dominant" means on a dark surface: not a near-white background, but a near-black one.
Why Not Pure Black?
Pure black (#000000) as a background causes halation — the phenomenon where high-contrast white text on black creates a visual blur or shimmer for many users. This is well-documented in OLED display behavior and is why nearly every major dark mode design system (Google Material, Apple HIG) uses dark gray, not black.
The standard approach:
#121212— Google Material Design's recommended dark surface baseline#1E1E1E— VS Code's default dark background, widely recognized as a comfortable dark surface#0F172A— Tailwind'sslate-950, works well as a dark dominant
All three are far from pure black and pass WCAG AA contrast requirements with light-colored text.
The Dark Mode CSS Variables Block
Here is the full CSS variables setup for a 60-30-10 dark mode palette, using prefers-color-scheme to switch automatically:
:root {
/* Light mode */
--color-dominant: #f8fafc;
--color-secondary: #1e293b;
--color-accent: #3b82f6;
}
@media (prefers-color-scheme: dark) {
:root {
/* Dark mode — separate values, same roles */
--color-dominant: #121212;
--color-secondary: #2d2d2d;
--color-accent: #60a5fa;
}
}
Three variables, two value sets. Every component referencing var(--color-dominant) switches automatically with the user's prefers-color-scheme system preference. No JavaScript required.
If you want user-controllable dark mode (toggle button) alongside the system preference, add a .dark class override and manage it with JavaScript:
:root {
--color-dominant: #f8fafc;
--color-secondary: #1e293b;
--color-accent: #3b82f6;
}
@media (prefers-color-scheme: dark) {
:root {
--color-dominant: #121212;
--color-secondary: #2d2d2d;
--color-accent: #60a5fa;
}
}
/* Manual override — wins over media query */
.dark {
--color-dominant: #121212;
--color-secondary: #2d2d2d;
--color-accent: #60a5fa;
}
.light {
--color-dominant: #f8fafc;
--color-secondary: #1e293b;
--color-accent: #3b82f6;
}
Toggle the class on <html> or <body> and store the preference in localStorage.
Text Contrast on Dark Backgrounds: WCAG AA Requirements
WCAG AA requires a 4.5:1 contrast ratio for normal text, 3:1 for large text (18px+ or 14px+ bold).
Common mistake: using pure white (#FFFFFF) text on #121212. The contrast ratio is 19.6:1 — technically sufficient but visually harsh. Extended reading on a pure-white-on-dark surface causes eye fatigue faster than a slightly softened alternative.
The standard approach is to use off-white text:
| Text color | Background | Contrast ratio | WCAG AA |
|------------|------------|----------------|---------|
| #FFFFFF | #121212 | 19.6:1 | Pass (harsh) |
| #E8EAED | #121212 | 15.8:1 | Pass (comfortable) |
| #94A3B8 | #121212 | 7.1:1 | Pass (muted) |
| #64748B | #121212 | 4.0:1 | Fail |
#E8EAED (Google's Material Design light text on dark surfaces) is a reliable default. It reads as white to users but reduces the harsh contrast edge.
For your dominant and secondary color combinations, verify contrast using the WebAIM Contrast Checker or run it in your browser's DevTools accessibility panel.
Accent Color Adjustment for Dark Surfaces
Saturated accent colors that work at 10% on light backgrounds can burn on dark surfaces. #3b82f6 (Tailwind blue-500) at full saturation on #121212 reads as neon — it draws too much attention and loses the 10% scarcity effect.
Shift to a lighter, slightly desaturated variant:
/* Light mode accent */
--color-accent: #3b82f6; /* blue-500 — works on light background */
/* Dark mode accent */
--color-accent: #60a5fa; /* blue-400 — lighter, less harsh on dark surface */
The contrast ratio of #60a5fa on #121212 is 8.6:1 — comfortably above WCAG AA, without the neon effect.
General rule: on dark surfaces, shift your accent one step lighter on the shade scale. blue-500 → blue-400. violet-600 → violet-400. emerald-500 → emerald-400.
Tailwind dark: Class Integration
Tailwind's dark: variant applies classes when the system preference is dark (or when a .dark class is present, depending on your darkMode config).
If you've registered your palette in @theme (Tailwind v4):
@theme {
--color-dominant: #f8fafc;
--color-secondary: #1e293b;
--color-accent: #3b82f6;
}
Override the values in a .dark context:
.dark {
--color-dominant: #121212;
--color-secondary: #2d2d2d;
--color-accent: #60a5fa;
}
Then in your JSX, you reference the semantic utilities without any dark-specific class names:
<div className="bg-dominant">
<nav className="bg-secondary">
<button className="bg-accent text-white">
Create project
</button>
</nav>
</div>
The CSS variable swap handles the dark mode switch — your component markup stays clean.
For Tailwind v3 with darkMode: 'class' in your config, you can still use this CSS variable approach. The dark: utility classes are available when you need element-specific overrides, but the variable swap pattern handles the bulk of color switching at the :root level.
Generating Both Palettes
Dark mode requires two separate sets of hex values: light palette and dark palette. Instead of manually selecting values and checking contrast for both, generate both at sixtythirtyten.co — the CSS variables output gives you both blocks ready to paste.
For the complete CSS variables reference with all framework formats, see CSS Variables Color System Cheat Sheet.
For Tailwind-specific dark mode wiring (.dark class with @theme), see 60-30-10 Tailwind Color Palette.
If you're using shadcn/ui, its .dark class selector pattern maps directly to this approach — see shadcn/ui Color System: Map 60-30-10 to CSS Variables.
For the base 60-30-10 rule and how the proportions map to layout regions, see The 60-30-10 Rule: A Developer's Guide to UI Color Balance.
Two palette sets. One variable swap. System preference handled automatically.