Skip to content
ExamsLibraryFor agentsConnect your agent

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.

  1. 1Three numbers you can read
  2. 2Why not HSL?
  3. 3Building a ramp
  4. 4Colors a screen can't show
  5. 5Migrating a hex palette

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:1
One color, three numbers. The ramp underneath is built from the same hue.

Three 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.

Same number, every hue
redL 0.63
yellowL 0.97
greenL 0.87
cyanL 0.91
blueL 0.45
pinkL 0.70

HSL calls all six “50% light”. Your eye disagrees: yellow glows, blue sinks.

The gray bars measure how light each swatch really looks.

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

my-app — claude
~/my-app❯
Reading src/styles/tokens.css
--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 below
color: currentColorunchangedkeywords never convert
! --muted on --page measures 2.8:1. Body text needs 4.5:1. Lower its L to 0.55?
The agent converts values, keeps keywords, and flags a contrast problem instead of quietly fixing it.

The rules it followed:

  1. Hex first, then rgb()/rgba(), then hsl().
  2. Alpha goes behind a slash: rgba(255,255,255,.12) becomes oklch(1 0 0 / 0.12).
  3. Never convert keywords (currentColor, transparent, inherit) or anything a third-party library reads as hex.
  4. Re-check contrast on the pairs that matter, and report every changed line, old beside new.
Migrate a palette to OKLCH· for your agent
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?

What this lesson builds on and leads to, across every course.

  • Contrast you can read
  • One neutral, one accent
  • Shadows, borders and corners
  • Designing a dark theme