Dark Mode Palette Converter
Turn a light palette into a coherent dark one in OKLCH, contrast-checked and ready to paste.
Part of design tools.
Paste a :root block, or one name: color per line. Comments and non-color declarations are ignored.
OKLCH lightness of the page background.
Where the most raised card lands.
Saturation kept in dark mode; 0.8 tames the glare.
Text and accents are lightened until they reach it.
| Token | Role | Light | Dark | On main surface |
|---|---|---|---|---|
| bg | #ffffff | #0d0d0d | ||
| surface | #f5f6fa | #1a1a1d | ||
| surface-sunken | #e8eaf2 | #2c2e32 | ||
| text | #14161f | #e7eaf5 | 16.18:1AAA | |
| text-muted | #5b6072 | #dadff0 | 14.62:1AAA | |
| brand | #2563eb | #4579e4 | 4.73:1AA | |
| success | #15803d | #438b56 | 4.69:1AA | |
| danger | #b91c1c | #ca5449 | 4.51:1AAadjusted |
Text on surface
| Pair | Light | Dark |
|---|---|---|
| text / bg | 18.04:1AAA | 16.18:1AAA |
| text / surface | 16.70:1AAA | 14.46:1AAA |
| text / surface-sunken | 15.02:1AAA | 11.33:1AAA |
| text-muted / bg | 6.25:1AA | 14.62:1AAA |
| text-muted / surface | 5.79:1AA | 13.06:1AAA |
| text-muted / surface-sunken | 5.20:1AA | 10.23:1AAA |
Light
Ship it in the dark
The quick brown fox jumps over the lazy dog, twice, to fill this paragraph with enough words to judge the contrast.
Primary actionDark
Ship it in the dark
The quick brown fox jumps over the lazy dog, twice, to fill this paragraph with enough words to judge the contrast.
Primary action:root {
--bg: #ffffff;
--surface: #f5f6fa;
--surface-sunken: #e8eaf2;
--text: #14161f;
--text-muted: #5b6072;
--brand: #2563eb;
--success: #15803d;
--danger: #b91c1c;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0d0d0d;
--surface: #1a1a1d;
--surface-sunken: #2c2e32;
--text: #e7eaf5;
--text-muted: #dadff0;
--brand: #4579e4;
--success: #438b56;
--danger: #ca5449;
}
}Why inverting a palette does not work
The two usual shortcuts both fail, and for different reasons. Inverting the RGB channels changes the hue: a blue brand comes back orange, which is the one thing a dark theme must not do. Flipping the HSL lightness keeps the hue but not the balance, because HSL lightness is arithmetic on RGB channels rather than anything the eye reads — a yellow and a blue that looked equally bright end up far apart. This converter works in OKLCH, where lightness is perceived lightness, so hue survives and the ramp keeps its order.
One rule per role
A surface, a text color and an accent do not convert the same way, which is why each token gets a role first. Surfaces have their lightness reflected into a narrow dark band — L' = Lmin + (1 − L) × (Lmax − Lmin) — because a dark UI has far less room than a light one: a background at 0.16 with cards at 0.30 already reads as a hierarchy. Text is reflected into a high band, 0.85 to 0.96, never pure white: white on near-black halates and is the most reported dark-mode complaint. Accents gain lightness and lose about a fifth of their chroma, since the saturation that was pleasant on white glares on black.
Contrast is checked, not assumed
Every text and accent is measured against the darkest surface with the same WCAG maths as the contrast checker, and any token short of the target has its lightness raised — and only its lightness, so hue and chroma stay put — until it passes. Those tokens are flagged adjusted. When the target cannot be met inside the sRGB gamut, you get the closest approximation plus an explicit warning rather than a silent failure. Translucent tokens are composited over their surface before measuring, which is the only way an alpha color has a contrast ratio at all.
Edge cases it already handles
A palette that is already partly dark is detected token by token: an input darker than mid-lightness is kept rather than brightened, and marked already dark. Pure white and pure black come out soft, never #000 as a surface or #fff as text. A palette with no text at all simply reports no pair instead of inventing one. To build the light palette in the first place, the palette generator turns one brand color into an eleven-step scale you can paste straight in here.
Pasting the export
The default export writes the light values on :root and the dark ones inside @media (prefers-color-scheme: dark), so a page follows the operating system with no JavaScript. The data-theme variant is for apps with a manual toggle: same tokens, scoped to an attribute you set on the root element. The JSON export carries both themes keyed by token name, ready for a design-token pipeline.