CSS Relative Color Syntax: Deriving Colors from Colors
Relative color syntax lets CSS derive a new color from an existing one by adjusting its channels directly, no preprocessor or JS required.
Relative color syntax is a CSS feature that lets you derive a new color from an existing one by referencing and adjusting its individual channels directly in a color function, instead of hardcoding a second hex value or reaching for a preprocessor. Written as rgb(from <color> r g b / a), hsl(from <color> h s l / a), or the equivalent for other color functions, it turns “make this color 20% darker” from a build-time calculation into something the browser does natively at paint time.
What problem this solves
Design systems constantly need color variations — a hover state that’s a shade darker than the base color, a border that’s the same hue but lower saturation, a background tint derived from a brand color at low opacity. Historically this meant one of three things: hardcoding every variant as its own hex value (which breaks the moment the base color changes), computing variants in a preprocessor like Sass at build time (which can’t respond to a runtime theme change), or computing them in JavaScript (which means a layout shift or flash while the calculation runs).
Relative color syntax moves this calculation into CSS itself, where it can reference CSS custom properties and respond to them changing at runtime — including a user switching between light and dark mode, or a theme color set dynamically via JavaScript.
The syntax
The general shape is <color-function>(from <origin-color> <channel1> <channel2> <channel3> / <alpha>). The origin color can be a literal, a custom property, or any valid color value. Inside the function, you get named variables for each channel of that color space and can reference or manipulate them with calc():
:root {
--brand: oklch(65% 0.2 250);
}
.button {
background: var(--brand);
}
.button:hover {
/* same hue and chroma, 15 percentage points darker */
background: oklch(from var(--brand) calc(l - 0.15) c h);
}
.button-tint {
/* same color, 10% opacity */
background: rgb(from var(--brand) r g b / 10%);
}
Each color function exposes its own channel names — r g b for rgb(), h s l for hsl(), l c h for oklch(), and so on — so you pick the function based on which channels you want to manipulate. Adjusting lightness is more predictable in oklch() than hsl(), because OKLCH’s lightness channel is perceptually uniform; see the breakdown of OKLCH versus HSL and RGB for why that matters when you’re generating a whole palette of tints and shades programmatically.
Where it replaces older techniques
Sass functions like darken() and lighten() computed a fixed output color at build time and baked it into the compiled CSS. That output couldn’t adapt if the input changed — swapping a brand color meant recompiling the stylesheet. Relative color syntax keeps the relationship live: if --brand changes (a theme switch, a CSS variable updated by JavaScript, a light-dark() value resolving differently), every derived color recalculates automatically, because it’s all still just CSS being evaluated, not a value that was pre-computed once and frozen.
It’s also a natural companion to color-mix(), which blends two colors together rather than adjusting one color’s own channels — color-mix() answers “what’s 20% of the way between these two colors,” while relative color syntax answers “what does this one color look like with its lightness channel nudged.” They’re frequently used together: color-mix() to blend toward white or black for a tint or shade, relative syntax to make a precise, single-channel adjustment.
A practical pattern: generating a color scale from one token
A common design-system need is a full lightness scale — 50, 100, 200, … 900 — generated from a single brand color rather than nine hand-picked hex values that drift out of sync over time:
:root {
--brand: oklch(55% 0.18 260);
--brand-100: oklch(from var(--brand) 0.95 c h);
--brand-300: oklch(from var(--brand) 0.8 c h);
--brand-500: var(--brand);
--brand-700: oklch(from var(--brand) 0.4 c h);
--brand-900: oklch(from var(--brand) 0.2 c h);
}
Change --brand once, and every step of the scale recalculates from it, because each one is defined relative to the source rather than as an independent literal.
Combining it with media queries and container-aware theming
Because relative color syntax resolves at paint time rather than build time, it composes naturally with other runtime-responsive CSS. A custom property set differently inside a prefers-color-scheme: dark media query, for instance, automatically changes every color derived from it via relative syntax elsewhere in the stylesheet — there’s no need to write separate light-mode and dark-mode variants of each derived shade, since they all recompute from whatever the source token currently resolves to. The same is true for a brand color that varies per customer in a multi-tenant product, or a theme color read from a CSS custom property set by JavaScript at runtime rather than known ahead of time: the derived hover states, borders, and tints stay correct without the application code needing to recompute and re-inject a whole palette every time the base color changes.
Browser support and fallbacks
Support for relative color syntax landed across major browsers well after color-mix() and OKLCH itself, so it’s worth checking current baseline support before relying on it for anything load-bearing. For non-critical color adjustments (a hover tint, a subtle border variant), a straightforward fallback is declaring a static color first and the relative-syntax version second — browsers that don’t understand the second declaration simply ignore it and keep the first, no @supports block required, the same progressive-enhancement pattern used for other newer color features.
The takeaway
Relative color syntax lets CSS derive a new color from an existing one — lighter, more transparent, less saturated — by manipulating its actual channels inline, instead of hardcoding a second value or precomputing it at build time. Paired with OKLCH for perceptually predictable lightness adjustments and color-mix() for blending, it turns a handful of design tokens into a full, self-maintaining color system that updates automatically whenever the source color does.
Tagged
Keep reading
Takina · · 4 min read Container Query Units: cqw, cqh, and cqi Explained
Container query units like cqw and cqi size elements relative to their containing element, not the viewport — here's how and when to use them.
Takina · · 4 min read What Is CSS Subgrid? Nested Grid Alignment Explained
CSS subgrid lets a nested grid item inherit its parent's track sizing, so children can align to the same columns or rows across unrelated containers.
Takina · · 4 min read CSS field-sizing Property Explained
The CSS field-sizing property lets form controls like textareas grow to fit their content automatically, without JavaScript resize listeners.