One seed. A whole system.
Relative color syntax in CSS means one design token can produce your entire palette. No preprocessor. No JS. No separate color library.
CSS relative color syntax lets you derive new colors mathematically from existing ones — inside CSS, with no preprocessor, no JavaScript, no color library.
:root {
--brand: oklch(52% 0.18 30);
}
.button {
background: var(--brand);
color: oklch(from var(--brand) 98% 0.02 h); /* near-white tint of brand */
border-color: oklch(from var(--brand) calc(l - 10%) c h); /* 10% darker */
}
.button:hover {
background: oklch(from var(--brand) calc(l + 5%) c h); /* 5% lighter on hover */
}
The syntax: oklch(from var(--brand) l c h) takes a source color and exposes its lightness, chroma and hue as named variables you can modify arithmetically.
oklch is the important choice here. Unlike hsl, oklch is perceptually uniform — a 10% lightness step looks the same visual distance regardless of where you are in the color range. Yellow at 50% lightness in hsl looks much brighter than blue at 50% lightness. oklch corrects for that.
Browser support: Chrome 119+ (October 2023), Safari 16.4+ (March 2023), Firefox 128+ (July 2024). MDN reference. Use
@supports (color: oklch(from red l c h))for fallbacks.
A complete palette from one token
:root {
--rust: oklch(52% 0.18 30);
/* Tints — lighter versions */
--rust-100: oklch(from var(--rust) 96% 0.04 h);
--rust-200: oklch(from var(--rust) 88% 0.08 h);
--rust-300: oklch(from var(--rust) 76% 0.14 h);
/* Shades — darker versions */
--rust-700: oklch(from var(--rust) calc(l - 15%) c h);
--rust-800: oklch(from var(--rust) calc(l - 25%) c h);
--rust-900: oklch(from var(--rust) calc(l - 35%) c h);
/* Complementary — rotate the hue 180° */
--rust-complement: oklch(from var(--rust) l c calc(h + 180));
/* Analogous — adjacent on the wheel */
--rust-analogous: oklch(from var(--rust) l c calc(h + 30));
}
Change --rust to any other oklch color. Every derivative updates. No Sass function. No color.js import. No design tool export step.
The alpha channel
.overlay {
/* 40% opacity version of the brand color */
background: oklch(from var(--brand) l c h / 40%);
}
This is the readable replacement for the color-mix() alpha trick:
/* color-mix approach */
background: color-mix(in oklch, var(--brand) 40%, transparent);
/* relative color approach */
background: oklch(from var(--brand) l c h / 40%);
Both work. The relative color version expresses the intent directly: “this color at 40% opacity.”
What you can’t do yet
- Fallbacks for older browsers require
@supports (color: oklch(from red l c h))— verbose but functional. - Derived colors in custom properties require
@propertyregistration if you want them to animate (the browser needs to know the type). - HSL relative colors work too, but don’t use them. Perceptual uniformity is the point.
The preprocessor solved a real problem. So did the color library. Both introduced a compile step, a build artifact, a dependency version to maintain. When the platform gives you the same result without the overhead, the honest move is to use it.
One seed. A whole system.