Guide
OKLCH tokens, and what a hex code cannot say
A hex code is an opaque number. OKLCH writes the same color as lightness, chroma and hue, so a hover state reads as the base with the lightness moved rather than another magic number.
6 min read
A hex code is a number that happens to render as a colour. It says nothing about the colour next to it — whether they share a hue, which one is lighter, or whether the second was derived from the first at all. That is fine for a swatch and expensive for a token, because a token set is mostly relationships: a hover state, a disabled state, a dark-mode step, a border meant to be the surface one shade down.
The same colour, written twice
Take a brand blue and the step a hover state would reach for. As hex, the pair is two unrelated facts:
--color-primary: #2563eb--color-primary-hover: #1f53c5
In OKLCH, it is one decision and its consequence:
--color-primary: oklch(0.54615 0.21521 262.881)--color-primary-hover: oklch(0.48137 0.18663 262.9)
The three numbers are lightness, chroma and hue. Read the two lines against each other and the relationship is on the page: lightness drops by 0.065, chroma by 0.029, and hue moves by 0.019 — a rounding artefact rather than a decision, since the ramp is only allowed to move lightness and chroma. Holding the hue is what makes the darker step still look like the brand rather than like some other colour that happens to be darker, and a reviewer can see that in the numbers without opening a colour picker.
Why this is a debt question
Colour debt rarely arrives as a decision. It arrives as eleven hex codes that were each correct on the day they were pasted in, with nothing in the file recording that four of them were meant to be one colour at four lightnesses. Six months later nobody can tell which are load-bearing, so none of them get deleted.
A token written as oklch(…) carries its own derivation. The hover state is visibly the base with the lightness moved; a value that does not fit the pattern stands out in review, and a check can assert the relationship rather than diffing opaque strings. That is the argument — not that OKLCH is a nicer colour space, but that it is a readable one. Colour debt is what accumulates when the format cannot say what you meant.
Where the switch appears, and where it does not
Every export surface offers HEX or OKLCH, but the toggle only appears on the formats the choice actually changes:
- CSS
- SCSS
- Tailwind v4
- Tokens
The omissions are deliberate. Tailwind v3 rewrites colours to rgb(… / <alpha-value>) so that bg-primary/50 works, and it cannot do that with an oklch() string — the config would load and then silently break every opacity utility in the project, which is worse than not offering the option. JSON and HEX are data rather than stylesheets, and their consumers expect the hex strings they have always been handed.
The plain-CSS export writes each token twice, hex first and oklch() second. A browser that cannot parse the second declaration discards it and keeps the first; one that can takes the later of the two. One extra line per token buys a correct colour everywhere instead of an unstyled one on an old browser. Tailwind v4 skips that pair on purpose — it already requires Safari 16.4, Chrome 111 or Firefox 128 and ships its own palette as oklch(), so a fallback there would guard against a browser that cannot run the framework the file is for. SCSS skips it for a different reason: Sass variables are not the cascade, so a second assignment replaces the first rather than being ignored, and the “fallback” would be a lie.
The precision is measured, not chosen
Tailwind writes its own palette at three decimal places and is right to — that palette is authored in OKLCH, so nothing can drift from it. This one is hex-native, which makes the OKLCH output a translation, and a translation that does not survive the round trip is the tool quietly changing the colour you picked. Three decimals moves 21% of colours by up to 8/255.
So lightness and chroma are written with five decimals and hue with three, which round-trips exactly across more than 200,000 colours including the full grey ramp, and a test pins it. Greys are the edge case worth knowing about: hue is meaningless at zero chroma, so the conversion drops it and CSS renders that as the keyword none — valid, but it reads as a mistake in a token file and behaves differently under relative colour syntax. The export writes 0 instead.
When hex is still the right answer
When the file is not a stylesheet. A brand guide, a handoff to someone whose tool only takes hex, a spreadsheet, a printer — hex is the universal format and nothing here is an argument against it. The claim is narrower than “OKLCH is better”: in a file whose job is to express how colours relate to each other, use the notation that can express it.