THE BROWSER

prefers-color-scheme landed in 2019 asking for palettes

prefers-color-scheme landed in 2019. Every dark build that looks wrong is making the same mistake.

One page shown light and dark side by side on two monitors
Two palettes, declared once and maintained twice — not one palette inverted.

The query and the error

The prefers-color-scheme media query — light and dark — shipped in Safari 12.1 in March 2019, followed by Firefox 67 in May 2019 and Chrome 76 in July 2019. By the time you read this it is everywhere. What is not everywhere is a correct understanding of what the preference asks for.

A dark studio, laptop open showing a dense dark product page, hand on the trackpad
Real system dark surfaces sit near #1e1e1e. Designing against pure black is designing against a machine nobody runs.

The query does not ask whether the user wants their colors inverted. It asks whether they prefer a dark-primary environment. Those are different problems, and confusing them is why so many dark builds feel cold, flat, or simply like a broken light build.

The inversion failure has a specific look: hard white text on pure black, every shadow removed, colored accent buttons that were already high-contrast on white now vibrating against a dark field. Pure #000000 backgrounds are rarely what system dark modes use — macOS Sequoia's dark UI settles around #1e1e1e to #2c2c2c; Windows 11's dark surfaces are in similar territory. Designing against #000 is designing against a system no one is actually running.

Building a second palette

What dark mode actually requires is a second considered palette. Shadows, which read naturally on light surfaces because light comes from above, need rethinking: you cannot drop-shadow a dark card onto a dark background with the same black-alpha value. Elevation that was expressed with shadow may need to become a subtly lighter surface color instead — this is the pattern Material Design documented, and W3C color contrast guidance applies regardless of mode.

Designing against #000 is designing against a system no one is actually running.

Typographic weight shifts, too. A regular-weight face at 16 px on white has different apparent density on a near-black surface; many practitioners find that font-weight benefits from a slight reduction in dark contexts, or at minimum testing, rather than a straight carry-over.

A screen showing a token palette beside a component, both legible
One token name resolving to two values is what makes a second palette maintainable rather than duplicated.

Color semantics are the hardest part. A brand red that reads as "action" on light might read as "warning" or simply as aggressive under dark. Stripe's dark mode, refined across multiple product years, uses desaturated tints of primary colors — nudged so they retain brand feel without the harshness of full saturation on a dark field.

Where design tokens are in play, this is manageable without duplication: a token like --color-surface-primary resolves to one value in the light block and another in the dark block. The structure is declared once; the palette is maintained twice. That is not overhead — it is the cost of having two correct palettes instead of one correct one and one accident.