Generating hover states, tints and themes from one brand colour removes hand-maintained colour tables.
CSS colour has moved well beyond sRGB hex.
Colour spaces. rgb() and hsl() describe sRGB, which is non-perceptual: hsl(60 100% 50%) yellow looks far lighter than hsl(240 100% 50%) blue at the same "lightness". oklch(L C H) and oklab() are perceptually uniform: equal L means equal perceived lightness, which makes accessible palettes and consistent contrast much easier. color(display-p3 1 0 0) and high-chroma oklch values reach wide-gamut colours on modern screens. Out-of-gamut colours are mapped to what the display can show, so give an sRGB fallback where precision matters.
color-mix() blends two colours in a chosen space: color-mix(in oklch, var(--brand), white 20%). Mixing in oklab or oklch avoids the muddy grey midpoints of sRGB mixing.
Relative colour syntax derives a colour from another: oklch(from var(--brand) calc(l - 0.1) c h) is a darker brand for hover, and rgb(from var(--brand) r g b / 50%) adds transparency. One token generates a whole scale.
light-dark() picks a value by the element's color-scheme: color: light-dark(#111, #eee), with color-scheme: light dark on :root.
Interpolation space matters in gradients too: linear-gradient(in oklch, blue, yellow) avoids the grey middle that sRGB gradients produce.
Finally, check contrast: perceptual lightness helps, but WCAG contrast ratios must still be checked with tools.
Example
:root {
color-scheme: light dark;
--brand: oklch(55% 0.2 265); /* indigo */
--brand-hover: oklch(from var(--brand) calc(l - 0.08) c h);
--brand-soft: color-mix(in oklch, var(--brand), white 85%);
--brand-50a: oklch(from var(--brand) l c h / 50%);
--bg: light-dark(#ffffff, #0b1020);
--fg: light-dark(#111827, #e5e7eb);
}
body { background: var(--bg); color: var(--fg); }
.button { background: var(--brand); }
.button:hover { background: var(--brand-hover); }
.tag { background: var(--brand-soft); }
.hero { background: linear-gradient(in oklch, oklch(70% 0.2 30), oklch(70% 0.2 250)); }
@media (color-gamut: p3) {
.badge { background: color(display-p3 1 0.2 0.4); }
}Remember
- oklch is perceptually uniform: equal L looks equally light
- color-mix(in oklch, ...) blends without muddy midpoints
- Relative colour syntax derives hover and alpha variants from one token
- light-dark() + color-scheme for simple theming
Watch out for
- Assuming equal hsl lightness means equal perceived lightness or contrast.