Engineering

Light and dark, both first-class

Dark mode is often treated as a switch someone flips near the end — invert the background, darken a few panels, ship it. The result is the familiar half-built dark theme, and it reads as an afterthought because that is exactly what it was.

Dark mode is not a toggle

You have seen the half-built version: text that sits a shade too dim to read comfortably, a shadow that vanishes into the dark it was drawn on, one stubborn white card glowing in a room that has gone quiet. Each is a small tell that the dark theme was an inversion applied late, not a design decided early.

Treating the two themes as equals is a different decision, made at the start. Neither is the "real" design with the other bolted on; both are first-class, and a component is not finished until it is right in both. That framing costs almost nothing when it is the starting assumption and a great deal when it is a retrofit — the same lesson accessibility teaches, in another key.

Tokens only, no raw values

The mechanism that makes two equal themes tractable is tokens. No colour is written as a raw hex value inside a component; every colour is a named token — surface, text, border, accent, the muted background behind a card — and a theme is nothing more than a second set of values for those same names. Switching themes swaps the values, not the components. The button does not know which theme it is in, and does not need to.

The discipline is absolute or it is nothing. One raw #ffffff hardcoded in a corner is the white card that will not turn off — the single value the theme switch cannot reach. So the rule is zero raw values in components: tokens for every colour, both themes defined for every token, and a lint that fails the moment a bare hex sneaks in. A single source of truth for colour is what lets two themes stay honest with each other.

Tested in both, across breakpoints

Two themes double the surface that can break, and neither can be verified by remembering to look. Contrast that passes in light can fail in dark and the reverse; a layout can be right on a wide screen and wrong on a phone in exactly one theme. So the check runs the full grid — light and dark, mobile and desktop — against the built page, and a component earns "done" only when every cell of that grid passes.

We hold this on our own work: Koryvant renders both themes as a matter of course, because a design system that only exists in light is only half a design system. It is the same standard we bring to software we build for others, and it sits right beside the accessibility bar in accessibility is a requirement — quality that stays verifiable in every theme a real person will actually use.

Questions

People also ask

    Why not just add a dark mode at the end?

    Because a bolted-on dark theme almost always leaks — dim text, lost shadows, a stray light-mode panel. Designing both themes as equals from the start, with colour driven by tokens rather than raw values, is far cheaper than retrofitting and produces a theme that actually holds together.

    What does 'tokens only' mean in practice?

    That no component contains a raw colour value. Every colour is a named token, and each theme supplies its own values for those names, so switching themes swaps values rather than components. A lint rule that rejects bare hex values keeps the single source of truth intact.