CSS Relative Color Syntax: Auto-Generate Palettes from One Token

CSS relative color syntax — color(from baseColor channel1 channel2 channel3) — lets you derive new colors from existing ones by manipulating individual channels. It replaces complex color-mix() workarounds and makes it trivial to build tints, shades, and transparent variants from a single design token.

Problem: CSS colour palettes require maintaining dozens of manually derived tints, shades, and transparent variants — changing the brand colour means updating every hex value across the stylesheet independently.

Solution: Use the CSS Relative Color Syntax (oklch(from var(--color) calc(l + 0.1) c h)) to derive tints and shades from a single custom property. Combine with CSS color-mix() for blends. All variants update automatically when the base variable changes, eliminating manual palette maintenance.

The examples below lighten and darken a brand color using relative oklch syntax, generate hover states automatically, and create a full CSS palette from a single custom property.

/* Relative color syntax: color(from      / ) */

:root {
    --brand: oklch(55% 0.18 250);  /* single source of truth */
}

/* Lighten: increase lightness channel by 15% */
.btn-light {
    background: oklch(from var(--brand) calc(l + 0.15) c h);
}

/* Darken: decrease lightness */
.btn-dark {
    background: oklch(from var(--brand) calc(l - 0.15) c h);
}

/* Desaturate: reduce chroma */
.btn-muted {
    background: oklch(from var(--brand) l calc(c * 0.4) h);
}

/* Complementary color: rotate hue by 180° */
.btn-complementary {
    background: oklch(from var(--brand) l c calc(h + 180));
}

/* Transparent variant: set alpha channel */
.overlay {
    background: oklch(from var(--brand) l c h / 0.2);
}

/* Works with any color space — hsl example */
.btn-hsl-light {
    background: hsl(from var(--brand) h s calc(l + 20%));
}

/* Generate automatic hover state without JavaScript */
.btn {
    background: var(--brand);
    transition: background 0.2s;
}
.btn:hover {
    background: oklch(from var(--brand) calc(l + 0.08) c h);
}

Build a complete palette from one token:

/* Auto-generate a 9-step palette from --brand */
:root {
    --brand: oklch(55% 0.18 250);

    --color-50:  oklch(from var(--brand) 0.97 calc(c * 0.1) h);
    --color-100: oklch(from var(--brand) 0.93 calc(c * 0.2) h);
    --color-200: oklch(from var(--brand) 0.85 calc(c * 0.4) h);
    --color-300: oklch(from var(--brand) 0.76 calc(c * 0.6) h);
    --color-400: oklch(from var(--brand) 0.67 calc(c * 0.8) h);
    --color-500: var(--brand);                                    /* base */
    --color-600: oklch(from var(--brand) calc(l - 0.08) c h);
    --color-700: oklch(from var(--brand) calc(l - 0.16) c h);
    --color-800: oklch(from var(--brand) calc(l - 0.24) c h);
    --color-900: oklch(from var(--brand) 0.20 calc(c * 0.5) h);
}

/* Theme switching — change only the token, all palette updates automatically */
[data-theme="green"] { --brand: oklch(55% 0.17 145); }
[data-theme="red"]   { --brand: oklch(52% 0.22  25); }

NOTE: Relative color syntax is supported in Chrome 119+, Safari 16.4+, and Firefox 128+. Use @supports (color: oklch(from red l c h)) to progressively enhance — fall back to hardcoded palette values for older browsers. This feature eliminates most uses of Sass color functions like lighten() and darken() in modern CSS.

Leave Comment

Your email address will not be published. Required fields are marked *