The naive dark mode swaps white for black and calls it done. The result is technically dark and unmistakably wrong: glowing text, floating shadows, colors that vibrate. Dark themes are not inverted light themes—they are a different physics.

The rules that change

Never pure black. #000 against light text creates maximum-contrast shimmer and makes elevation impossible. Start around #121212 or, better, a hue-tinted dark like oklch(18% 0.015 285).

Elevation becomes lightness. In light mode, higher surfaces cast bigger shadows. On dark backgrounds shadows are nearly invisible—so higher surfaces get lighter instead. A modal should sit several lightness steps above the page.

:root {
  --surface-0: oklch(18% .015 285);  /* page */
  --surface-1: oklch(22% .015 285);  /* card */
  --surface-2: oklch(27% .015 285);  /* modal */
  /* desaturated brand for dark surfaces */
  --brand: oklch(72% 0.13 285);
}

Desaturate everything. Saturated colors that look confident on white become fluorescent on dark. Drop chroma and raise lightness on every accent. Text follows the same rule: not pure white but oklch(92%), with secondary text a couple of steps down.

“Light mode is paper. Dark mode is a screen in a dim room. Different materials, different rules.”

Ship it as a system

If your components reference semantic tokens—--surface, --text-strong, --accent—dark mode is one prefers-color-scheme block redefining them. If they reference raw hexes, dark mode is a rewrite; fix the tokens first. Set color-scheme: light dark so scrollbars and form controls follow along, and test at low screen brightness in a dark room—where dark mode is actually used, and where every mistake glows.