oklch(from …)
Colours that go together, and which of them you can put text on
A wheel tells you what is related. It does not tell you what is legible, which is the half that decides whether a palette survives contact with an interface. Pick one colour: every harmony below is one hue rotation away from it, corrected for the fact that sRGB holds a vivid blue and a vivid yellow-green at very different lightnesses — and every swatch is measured on screen and says to your face whether text can sit on it.
This browser has no relative colour syntax, so every swatch above is the base colour and nothing else. The rest of the page still works.
CaveatEqual chroma at every hue is a lie sRGB will not honour: there is far less room for a vivid yellow-green at a given lightness than for a blue, so the browser quietly maps the difference away and the wheel's triad comes out as khaki. The rows above correct for it — each rotated hue is given back the lightness its own hue can carry, which is the cos() term in the snippet below — but it is an approximation of the gamut, not a measurement of it. Judge the row on screen, never on the numbers. The contrast figures are read off the rendered swatch for exactly that reason.
FallbackThe picker is <input type="color">, which is sRGB hex and nothing else: you cannot start from a P3 colour here even though oklch() could describe one. Without relative colour syntax every swatch falls back to the base colour, and the page says so above rather than showing a row of identical squares with no explanation.
.palette { --base: #3d5afe; }
/* a hue rotation, plus the lightness
sRGB owes you for landing on a
yellow-green instead of a blue */
.sw[data-dh] {
background: oklch(
from var(--base)
clamp(0.2, calc(l + 0.14 * (
max(0, cos((h + var(--dh)
- 100) * 1deg))
- max(0, cos((h - 100) * 1deg))
)), 0.93)
c calc(h + var(--dh)));
}
/* … or a new lightness outright,
with the chroma pulled back */
.sw[data-tone] {
background: oklch(
from var(--base)
var(--l) calc(c * var(--cm)) h);
}