Everything you need to know about CSS Relative Color Syntax in 2026 — from basic syntax to building complete design token systems. Practical code examples, real-world patterns, and migration strategies from a senior frontend developer.
For years, if you wanted to derive a lighter variant of a brand color, a semi-transparent
overlay, or a complementary hue for a hover state, you had two options: Sass
functions (like lighten(), darken(), saturate())
or manually computing hex values in a color picker. Both approaches are
static — you decide the derived color at build time or design time, and it never changes.
CSS Relative Color Syntax changes this fundamentally. It lets you take
any color — a custom property, a named color, a hex value — and derive new colors from
it at runtime in the browser. This means your design tokens can be dynamic: a user
overrides --brand-color, and every hover state, border variant, and background
tint updates automatically.
In this guide, I'll cover the full syntax, all practical color manipulation patterns, how to build design token systems with relative colors, and how to migrate from Sass to native CSS color functions. By the end, you'll be able to eliminate color preprocessors from most of your projects.
CSS Relative Color Syntax allows you to create a new color based on an existing
origin color. The key is the from keyword placed inside
any CSS color function:
/* General pattern */ background: rgb(from var(--brand) r g b); background: oklch(from #6366f1 l c h); background: hsl(from indigo h s l);
The browser takes the origin color, converts it to the target color space, and
destructures it into named channel variables. You can then use
these variables as-is, or manipulate them with calc():
/* Same color — no manipulation */ .exact-copy { background: rgb(from #6366f1 r g b); /* Same as #6366f1 */ } /* Manipulated — 20% lighter */ .lighter { background: oklch(from #6366f1 calc(l + 0.2) c h); }
💡 Key insight: The channel variable names depend on the output
color function, not the origin color. If you pass a hex value to oklch(),
the browser converts hex → OKLCH, then gives you l, c, h
variables. If you pass OKLCH to rgb(), you get r, g,
b.
You can use relative color syntax with any CSS color function — rgb(),
hsl(), hwb(), lab(), lch(),
oklch(), oklab(), and color(). But the choice
of color space dramatically affects the quality of your manipulations.
| Color Space | Channel Variables | Best For | Drawback |
|---|---|---|---|
rgb() |
r g b |
Simple opacity, direct value manipulation | Not perceptually uniform; RGB channels are unintuitive for hue shifts |
hsl() |
h s l |
Hue rotation, saturation adjustments | Lightness is not perceptually uniform — same delta at different hues looks different |
lch() |
l c h |
Perceptually uniform lightness and chroma | Can produce out-of-gamut colors for display (sRGB) |
oklch() |
l c h |
Everything — perceptual uniformity + sRGB-safe | Hue angle is slightly different from HSL (minor adaptation needed) |
lab() |
l a b |
Scientific color matching, fine-grained channel control | a and b axes are unintuitive for simple lighten/darken |
OKLCH is the recommended default for programmatic color manipulation.
It's perceptually uniform (same delta = same visual difference at any hue), it stays
within the sRGB gamut (no unexpected out-of-range values), and its l
(lightness), c (chroma), h (hue) channels map intuitively
to the operations developers actually need: lighten/darken (L), saturate/desaturate (C),
and hue shift (H).
Each color function exposes specific channel variable names. Here's a quick reference:
| Function | Channel 1 | Channel 2 | Channel 3 | Alpha |
|---|---|---|---|---|
rgb() |
r (0–255) |
g (0–255) |
b (0–255) |
alpha / a |
hsl() |
h (0–360) |
s (0–100%) |
l (0–100%) |
alpha |
hwb() |
h (0–360) |
w (0–100%) |
b (0–100%) |
alpha |
oklch() |
l (0–1) |
c (0–0.4) |
h (0–360) |
alpha |
oklab() |
l (0–1) |
a (−0.4–0.4) |
b (−0.4–0.4) |
alpha |
lab() |
l (0–100) |
a (−125–125) |
b (−125–125) |
alpha |
lch() |
l (0–100) |
c (0–150) |
h (0–360) |
alpha |
💡 Note: When using oklch(), the lightness l
ranges from 0 to 1 (0 = black, 1 = white), and chroma c typically ranges from
0 to about 0.37 for sRGB colors. A chroma of 0 produces gray. For hsl(),
saturation and lightness are percentages (0%–100%), and hue is degrees (0–360).
Let's explore the real-world color transformations you'll use most often. All examples use OKLCH as the primary color space.
Increase lightness (l) by adding a delta. In OKLCH, adding 0.05 produces
a noticeably but subtly lighter variant — consistent across all hues:
.card { background: oklch(from var(--surface) l c h); } .card:hover { /* 10% lighter */ background: oklch(from var(--surface) calc(l + 0.1) c h); }
Decrease lightness. In OKLCH, subtract from l:
.modal-overlay { /* 30% darker than the page background */ background: oklch(from var(--bg) calc(l - 0.3) 0.01 h); /* Near-black, desaturated overlay */ }
In OKLCH, chroma (c) controls saturation. Zero = gray (completely desaturated).
Increase for more vibrant, decrease for muted:
.muted-text { /* Desaturate the brand color for secondary text */ color: oklch(from var(--brand) l calc(c * 0.3) h); } .vivid-hover { /* Boost saturation on hover */ color: oklch(from var(--brand) l calc(c * 1.4) h); }
The alpha channel is available as alpha in all color functions:
.overlay { /* 30% opacity of the brand color */ background: oklch(from var(--brand) l c h / 0.3); } .on-scroll-header { /* Semi-transparent — 80% of original opacity */ background: oklch(from var(--header-bg) l c h / calc(alpha * 0.8)); }
Shift the hue to create complementary or analogous colors. Adding 180 degrees gives the complementary color:
.danger-button { background: var(--brand); } .danger-button:hover { /* Rotate hue by 180° to get complementary color */ background: oklch(from var(--brand) l c calc(h + 180)); } /* Analogous: shift by 30° */ .secondary-brand { color: oklch(from var(--brand) l c calc(h + 30)); }
A true inversion in OKLCH uses lightness complement (1 - l) and
hue rotation (add 180):
.inverted { background: oklch(from var(--surface) calc(1 - l) c calc(h + 180)); }
Generate a focus ring that works on any background color:
*:focus-visible { outline: 2px solid; /* Invert and lighten for visibility */ outline-color: oklch(from var(--bg) calc(1 - l) c calc(h + 180)); outline-offset: 2px; }
Here's where relative color syntax truly shines — creating an entire color system from a single base custom property:
:root { /* The single source of truth */ --brand: #6366f1; --brand-hover: oklch(from var(--brand) calc(l + 0.07) c h); --brand-active: oklch(from var(--brand) calc(l - 0.05) calc(c * 0.8) h); --brand-subtle: oklch(from var(--brand) calc(l + 0.2) calc(c * 0.3) h); --brand-bg: oklch(from var(--brand) calc(l + 0.35) calc(c * 0.15) h / 0.3); --brand-text: oklch(from var(--brand) calc(l - 0.15) calc(c * 0.5) h); --brand-border: oklch(from var(--brand) l calc(c * 0.6) h / 0.4); --brand-complement: oklch(from var(--brand) l c calc(h + 180)); } /* Dark theme — swap the base color, everything updates */ [data-theme="dark"] { --brand: #a5b4fc; /* All --brand-* tokens automatically recalculate */ }
Change --brand in one place, and every derived color — hover, active,
background, text, border, complement — updates automatically. This is impossible
with Sass, where derived colors are computed at build time and frozen into
the compiled CSS.
If you're currently using a Sass color system, here's how each operation maps:
| Operation | Sass | CSS Relative Color (OKLCH) |
|---|---|---|
| Lighten | lighten($color, 10%) |
oklch(from var(--c) calc(l + 0.1) c h) |
| Darken | darken($color, 10%) |
oklch(from var(--c) calc(l - 0.1) c h) |
| Saturate | saturate($color, 20%) |
oklch(from var(--c) l calc(c * 1.2) h) |
| Desaturate | desaturate($color, 30%) |
oklch(from var(--c) l calc(c * 0.7) h) |
| Adjust opacity | rgba($color, 0.5) |
oklch(from var(--c) l c h / 0.5) |
| Complement | complement($color) |
oklch(from var(--c) l c calc(h + 180)) |
| Mix | mix($a, $b, 50%) |
Not directly supported (use CSS Gradient or color-mix()) |
Most common operations have a direct equivalent. The one gap is mix() —
for that, CSS provides the color-mix() function as a separate feature.
Together, color-mix() and relative color syntax cover every practical
color operation you'd do in Sass.
Relative color syntax works seamlessly with CSS custom properties. This is where the dynamic nature truly shines:
.themed-card { /* Accept any brand color */ --card-brand: var(--accent); background: oklch(from var(--card-brand) calc(l + 0.35) calc(c * 0.1) h / 0.4); border: 1px solid oklch(from var(--card-brand) l calc(c * 0.5) h / 0.3); color: oklch(from var(--card-brand) calc(l - 0.2) calc(c * 0.6) h); } /* Usage in different contexts */ .card-primary { --card-brand: var(--brand); } .card-success { --card-brand: var(--success); } .card-warning { --card-brand: var(--warning); }
Each card type gets its own color system derived from a single custom property. The card component defines how colors relate, and the consumer defines which colors to use — pure separation of concerns.
While relative color syntax gives you fine-grained channel control,
color-mix() is better for blending two colors. They work beautifully
together:
.glass-card { /* Mix a desaturated version of brand with white */ --glass-base: oklch(from var(--brand) l calc(c * 0.2) h); background: color-mix(in oklch, var(--glass-base) 30%, white); }
As of May 2026, CSS Relative Color Syntax has ~90% global browser support:
For fallback support, use the CSS cascade — declare a fallback value first, then the relative color:
.btn-primary { /* Fallback for older browsers */ background: #818cf8; /* Modern browser override */ background: oklch(from var(--brand) calc(l + 0.07) c h); }
You can also use the @supports rule for conditional enhancement:
@supports (color: oklch(from red l c h)) { :root { --brand-hover: oklch(from var(--brand) calc(l + 0.07) c h); --brand-subtle: oklch(from var(--brand) calc(l + 0.2) calc(c * 0.3) h); } }
.btn { --btn-bg: var(--brand); background: var(--btn-bg); color: white; border: 1px solid oklch(from var(--btn-bg) calc(l - 0.1) c h); transition: background 0.2s; } .btn:hover { background: oklch(from var(--btn-bg) calc(l + 0.07) calc(c * 1.15) h); } .btn:active { background: oklch(from var(--btn-bg) calc(l - 0.05) calc(c * 0.9) h); } /* Just change --btn-bg for different button variants */ .btn-danger { --btn-bg: var(--danger); } .btn-success { --btn-bg: var(--success); } .btn-warning { --btn-bg: var(--warning); }
.auto-contrast-text { --bg: var(--section-bg); /* Dark text on light backgrounds, light text on dark */ color: oklch(from var(--bg) calc(1 - l) 0.02 h); }
CSS relative color syntax is computed at paint time by the browser's CSS engine. The performance characteristics are excellent:
color-mix() with custom properties on intermediate values
is generally lighter for animation use casesThe CSS Color Level 4 specification is still evolving, and relative color syntax is part of a larger ecosystem of modern color features. Here's what to watch for:
color-mix() — future versions
may support more than two colorsThe investment in learning relative color syntax and OKLCH today will pay off for years. As browser support approaches 100%, you'll be able to eliminate entire build-time color processing pipelines from your projects.
Ready to use CSS Relative Color Syntax in your project? Here's the simplest starting point:
/* Define your brand color */ :root { --brand: #6366f1; --brand-hover: oklch(from var(--brand) calc(l + 0.07) c h); --brand-muted: oklch(from var(--brand) calc(l + 0.2) calc(c * 0.35) h); --brand-bg: oklch(from var(--brand) calc(l + 0.35) calc(c * 0.1) h / 0.3); } /* Use them throughout your CSS */ .btn { background: var(--brand); } .btn:hover { background: var(--brand-hover); } .card { background: var(--brand-bg); } .muted-link { color: var(--brand-muted); }
That's it. Start with one base custom property and three derived variants. As you get comfortable, expand to full design token systems — you'll never reach for a color picker or Sass function again for most color operations.
CSS Relative Color Syntax is one of the most transformative CSS features of the 2020s. It eliminates the last major reason to use a CSS preprocessor — dynamic color manipulation — and puts the power of complete, runtime-adaptive design systems into native CSS.
Need help implementing a modern CSS color system in your project? I'm available for frontend architecture consulting and development. View my services or contact me directly.
I build production web applications using modern CSS and JavaScript. Let's discuss your project — free consultation.