Color you can do math with
beginner·8 min·Markdown for agents
What this lesson teaches
Read and write colors in OKLCH, build an even color ramp from one hue, and get an agent to migrate a hex palette without breaking it.
Drag the L slider below. The color gets lighter, and it stays the same blue. Now drag H: the hue spins round the wheel, and the color stays exactly as light. That is the whole trick of this lesson.
oklch(0.62 0.19 255)The ramp for this hue
--hue-600: oklch(0.52 0.13 255);White text on it: 5.5:1Three numbers you can read
Most colors on the web are written as : #3b82f6. A screen understands that. A person doesn't. You can't look at #3b82f6 and know whether it is light or dark, or what happens if you nudge it.
writes the same blue as oklch(0.623 0.188 260), three numbers in a row:
- L, : how bright it looks to a person, 0 (black) to 1 (white).
- C, : how vivid. 0 is gray; around 0.3 is the loudest most screens show.
- H, : which color, as an angle. Roughly 25 red, 90 yellow, 145 green, 260 blue, 330 pink.
Why not HSL?
You may have met , which also has a "lightness". The problem is that it lies. Toggle between the two rows below: at "50% lightness" HSL's yellow glows and its blue sinks. OKLCH's L 0.7 is one level across every hue.
HSL calls all six “50% light”. Your eye disagrees: yellow glows, blue sinks.
A ramp spaced on HSL inherits that lie: some steps jump, others barely move, and the light tints drift toward violet. Never generate a scale in HSL.
Building a ramp
A is one hue in eleven steps, named 50, 100, 200 … 900, 950. The recipe the skill teaches has four parts, and you can watch each one in the demo above.
Anchor L and space it evenly
Step 50 sits near L 0.97, step 950 near L 0.22, and the steps between are evenly spaced. Even spacing is the point: every neighbor looks equally far apart. Never run to pure 0 or 1, where the color stops reading as its hue.
Shape chroma as a curve
Give each step a share of the most chroma it can hold: about 30% at the ends, rising to about 85% around 400–500. Tints stay airy, shades stay rich.
Clamp per step
The ceiling moves with lightness, so a vivid blue necessarily loses chroma toward both ends. That is the geometry of color, not a bug.
Keep the hue fixed
One H for all eleven steps. If an inherited scale wanders more than about 10° end to end, rebuild it on its middle hue.
Colors a screen can't show
Push C up in the first demo and a warning appears: "past sRGB". OKLCH can name colors no ordinary screen shows. The range a screen can show is its , and its edge moves with hue and lightness. At L 0.5, violet can go to C 0.29 while cyan tops out near 0.09.
So two hues never share one chroma number. A brand blue, a success green and a warning amber feel like one family when each sits at the same L and the same share of its own ceiling.
When a value goes past the edge, clamp it yourself: keep L and H, lower C until it fits. Left alone, the browser maps it in a way you didn't choose.
.cta { background: oklch(0.66 0.2 330); } /* safe everywhere */
@media (color-gamut: p3) { .cta { background: oklch(0.66 0.32 330); } } /* extra punch on wide-gamut screens */That second line is for , the wider gamut on recent phones and Macs. The punch goes in the P3 layer. Raising the base color's chroma breaks it on every ordinary screen.
Migrating a hex palette
Moving an existing file to OKLCH is a value-for-value swap. Nothing else in the file should change: not the order, not the comments, not the gradient shapes. Watch an agent do it properly.
--brand: #3b82f6oklch(0.623 0.188 260)same color, new notation--overlay: rgba(255,255,255,.12)oklch(1 0 0 / 0.12)alpha after a slash--muted: #999999oklch(0.683 0 0)exact, but see belowcolor: currentColorunchangedkeywords never convertThe rules it followed:
- Hex first, then
rgb()/rgba(), thenhsl(). - Alpha goes behind a slash:
rgba(255,255,255,.12)becomesoklch(1 0 0 / 0.12). - Never convert keywords (
currentColor,transparent,inherit) or anything a third-party library reads as hex. - Re-check contrast on the pairs that matter, and report every changed line, old beside new.
Migrate the colors in src/styles/tokens.css to oklch(). - Value for value: hex first, then rgb()/rgba(), then hsl(). Alpha goes after a slash. - Keep comments, order and indentation; don't touch keywords (currentColor, transparent) or hex a library parses. - Clamp anything out of sRGB by lowering chroma, holding L and H. - Re-run contrast on text pairs and list any that fail 4.5:1. Don't fix them silently. - Report every changed line: old value, new value, one-clause reason.
Quick check
A brand teal and a brand violet should feel equally vivid. What do you copy between them?
In the skill
The nodes your agent reads when it does this work.
- MOC — SurfaceColor, shadow, border-radius, dark mode. The "background" choices that decide feel.
surface/MOC-surface - Color scales in OKLCHAuthor in OKLCH. One hue per ramp, L anchored 0.97→0.22 and spaced evenly, chroma as a curve clamped per step, siblings at the same fraction of their own ceiling, dark mode by remapping steps, contrast repaired by moving L only.
surface/color-scales-oklch - OKLCH gamut and migrationChroma ceilings swing with hue and lightness — clamp C holding L and H, layer P3 behind a media query over an sRGB base, add a hex floor only for pre-2023 browsers, and migrate hex/rgb/hsl to OKLCH value-for-value, reporting every change.
surface/oklch-gamut-and-migration
Connected lessons
What this lesson builds on and leads to, across every course.