Relative Color Syntax in Practice: Deriving Color Tones From CSS Variables
AI generated
{ }
@
CSS · Color · Design Tokens · Color Level 5
Relative Color Syntax in Practice
Deriving color tones directly from existing CSS variables

Relative Color Syntax lets you compute an entire palette in the browser from a single brand color, instead of maintaining hover, active and border colors as separate variables. With the from keyword, color space channels and calc(), maintainable color systems emerge directly in native CSS, with no Sass and no build step required.

18 min read from · oklch · calc() · custom properties Chrome 119+ · Safari 16.4+ · Firefox 128+

1. What Relative Color Syntax solves

Relative Color Syntax is part of the CSS Color Module Level 5 and answers a problem that shows up in every larger stylesheet: variants of a base color for hover, active, focus rings and borders need to exist without maintaining every variant as its own custom property. Before Relative Color Syntax there were only two ways to do this, either use a preprocessor such as Sass with functions like darken() and lighten(), or manually compute and maintain a separate hex or OKLCH variable for every single variant.

Both paths create maintenance overhead. If the brand color changes, every derived value in the manual approach has to be recomputed by hand. With Sass, an additional build step is required, and the resulting values can no longer change at runtime, which makes theming through JavaScript or through prefers-color-scheme much harder. Relative Color Syntax solves exactly that: the derivation happens directly in the browser, at runtime, from the current value of any color, whether it lives in a custom property, a literal, or the result of another color function.

2. Syntax in detail: from, channels and calc()

The basic form of Relative Color Syntax looks like this: oklch(from <color> l c h). The keyword from followed by any color opens access to its channels in the target color space. In oklch(), those are l for lightness, c for chroma and h for hue, each addressable under the same name used in the function call. If a channel should stay unchanged, its name is written as is, if it should change, it gets replaced with a computed expression or directly with calc().

What matters is that the source color may be written in any format, for example as a hex value, as rgb(), as hsl(), or as another Relative Color Syntax expression, while the target color space in the outer function determines which channel names are usable. This even allows color space conversion as a side effect, from a color defined as hex, its OKLCH channels can be read directly with oklch(from #7c3aed l c h), with no external conversion tool needed.


/* Basic Relative Color Syntax: read channels from an existing color */
:root {
  --brand: #7c3aed;
}

.button {
  /* Convert hex to oklch and read its channels directly */
  background: oklch(from var(--brand) l c h);
}

.button:hover {
  /* Reduce lightness by an absolute amount using calc() */
  background: oklch(from var(--brand) calc(l - 0.08) c h);
}

.button:active {
  /* Reduce lightness further, slightly desaturate too */
  background: oklch(from var(--brand) calc(l - 0.14) calc(c * 0.9) h);
}

A common pitfall with this pattern: l in oklch() is normalized to the range 0 to 1, while some tools output lightness as a percentage from 0 to 100. Anyone computing with calc(l - 8%) instead of calc(l - 0.08) gets a completely different result or an invalid value. Relative Color Syntax does support percentage values for l, c and h in some contexts, but staying consistent and predictable only works if a team commits to one unit per color space and documents it clearly.

3. Deriving hover and active states from a base color

The most common practical use of Relative Color Syntax is exactly the example from the previous section: deriving the interactive states of a component from its base color. Instead of maintaining three or four variables per button, a single custom property is enough, from which hover, active, focus and disabled states get computed. This does not just reduce the number of variables, it also guarantees that all states actually match the base color, because they are mathematically derived from it instead of being maintained independently by hand.

This becomes especially valuable in component libraries with many color variants, for example a button that exists as primary, danger, success or warning. Instead of defining a complete set of state colors for every variant, only the base color per variant is defined, and every state gets computed through the same Relative Color Syntax formula. If the desired contrast for hover states changes later on, a single adjustment to the formula in one place is enough, instead of correcting dozens of individual values.

4. Combining with oklch for uniform steps

Relative Color Syntax reaches its full potential when combined with the oklch() color space, because its lightness axis is perceptually uniform. Reducing l by the same amount darkens every starting color to the same perceived degree, regardless of whether the base color is blue, yellow or violet. That is not the case with hsl(), where the same lightness reduction produces perceptually different brightness changes depending on hue, because hsl() is not based on human color perception but on a simple cylindrical RGB model.

In practice this means a shading formula built with Relative Color Syntax and oklch() works equally well for any brand color, without needing individual adjustments per hue. This is the decisive advantage over older Sass functions like darken(), which often build on hsl() internally and therefore produce unexpectedly strong or weak contrast for certain hues.


/* Perceptually uniform tint and shade scale from one base color */
:root {
  --brand-500: oklch(58% 0.19 291);
}

.card {
  /* Consistent lightness steps regardless of hue */
  --brand-100: oklch(from var(--brand-500) 0.94 calc(c * 0.35) h);
  --brand-300: oklch(from var(--brand-500) 0.80 calc(c * 0.7) h);
  --brand-700: oklch(from var(--brand-500) calc(l - 0.16) c h);
  --brand-900: oklch(from var(--brand-500) calc(l - 0.32) calc(c * 0.6) h);

  background: var(--brand-100);
  border: 1px solid var(--brand-300);
  color: var(--brand-900);
}

5. Alpha channel manipulation without an extra variable

Besides lightness, chroma and hue, Relative Color Syntax also allows direct access to the alpha channel through the name alpha. This removes the need to maintain a second custom property in rgba or color function notation for transparent variants of a color. Instead of defining --brand-transparent: rgba(124, 58, 237, 0.15) as its own variable that can drift out of sync, the transparent value gets computed directly from the base color.

This is especially useful for overlay backgrounds, focus rings and subtle dividers that should all carry the same hue as the base color but with different opacity. With Relative Color Syntax, the relationship between a base color and its transparent variants always stays explicit in the code, instead of existing implicitly across two independently maintained variables that easily drift apart whenever the base color changes.


/* Alpha channel access via Relative Color Syntax */
:root {
  --focus-color: oklch(62% 0.21 264);
}

.input:focus-visible {
  outline: 2px solid var(--focus-color);
  /* Same hue, reduced opacity, no second variable required */
  box-shadow: 0 0 0 4px oklch(from var(--focus-color) l c h / 0.25);
}

.overlay {
  /* Derive a translucent backdrop directly from the brand color */
  background: oklch(from var(--brand-500) calc(l - 0.4) c h / 0.55);
}

6. Practical example: a full palette from one brand color

In a real design system project, Relative Color Syntax can be used to generate a ten step palette from exactly one brand color, similar to Tailwind CSS scales, but without pre-generated static values. The advantage over a palette baked into the build process: if the brand color changes during a rebrand, only the single base variable needs to be updated, all derived steps update automatically, with no rebuild at all.

This technique works exceptionally well for multi tenant applications, where every customer supplies their own brand color. Instead of computing a complete palette per customer and shipping it as a separate CSS file, a single custom property per tenant plus one shared Relative Color Syntax formula is enough to generate the right shades for every tenant automatically.


/* Ten-step palette derived from a single tenant brand color */
:root {
  --tenant-brand: oklch(55% 0.20 258);

  --color-50:  oklch(from var(--tenant-brand) 0.97 calc(c * 0.15) h);
  --color-100: oklch(from var(--tenant-brand) 0.93 calc(c * 0.3)  h);
  --color-300: oklch(from var(--tenant-brand) 0.82 calc(c * 0.65) h);
  --color-500: var(--tenant-brand);
  --color-700: oklch(from var(--tenant-brand) calc(l - 0.14) c h);
  --color-900: oklch(from var(--tenant-brand) calc(l - 0.30) calc(c * 0.7) h);
}

7. Browser support, fallbacks and @supports

Chrome, Edge and Safari have fully supported Relative Color Syntax since 2023 and 2024 respectively, and Firefox followed with version 128. For projects that still need to serve older browsers, a fallback via @supports is recommended, where fixed hex or OKLCH values act as a static reserve outside the supported block. Important detail: @supports (color: oklch(from red l c h)) specifically tests the Relative Color Syntax capability, not just general oklch() support, since some browsers implemented oklch() without from earlier on.

In practice this fallback is usually enough: older browsers see a solid, manually maintained base palette, modern browsers get the dynamically computed version with all the benefits of Relative Color Syntax. Since color shading is rarely business critical, a visually slightly different but functionally correct fallback is an acceptable trade off in the vast majority of projects.


/* Fallback for browsers without Relative Color Syntax support */
.button {
  background: #6d28d9; /* static fallback shade */
}

@supports (color: oklch(from red l c h)) {
  .button {
    background: oklch(from var(--brand-500) l c h);
  }

  .button:hover {
    background: oklch(from var(--brand-500) calc(l - 0.08) c h);
  }
}

8. Maintainability compared to Sass functions

Sass functions like darken(), lighten() and mix() were considered the standard solution for derived colors for years, but they have a structural downside: they get evaluated at build time and produce static values in the shipped CSS. A runtime theme switch, for example through prefers-color-scheme or a manual dark mode toggle, can no longer influence these values, because they are already baked into the stylesheet. Relative Color Syntax solves exactly this problem, because the computation happens in the browser and is automatically re-evaluated whenever the referenced custom property changes.

Another benefit: with Relative Color Syntax, the dependency on a build tool for pure color calculations disappears. Smaller projects without a Sass toolchain benefit especially, since they now reach the same functionality in plain CSS, without introducing node-sass, Dart Sass or an extra PostCSS step. For design systems that already rely on custom properties, Relative Color Syntax is the logical native next step.

9. Relative Color Syntax in direct comparison

The following table compares the three common approaches to color derivation and shows which approach is the better choice in which situation.

Criterion Sass darken/lighten Relative Color Syntax Manual variables
Runtime update No, static Yes, live in browser No, manual
Build step required Yes No No
Perceptually uniform Rarely (hsl based) Yes, with oklch Depends on the author
Number of variables Low Minimal High
Browser support 2026 Not relevant (build time) All modern engines Universal

In new projects targeting modern browsers, Relative Color Syntax is almost always the right choice, because it reduces build dependencies while enabling runtime theming. Existing projects with a mature Sass toolchain do not need to migrate immediately, but should prefer building new color functions with Relative Color Syntax going forward, to gradually become less dependent on build time computation.

Mironsoft

CSS architecture, design tokens and modern color systems

A color palette that maintains itself?

We build color systems with Relative Color Syntax and oklch that derive consistent, perceptually uniform palettes automatically from a single brand color per tenant, with no build step and no duplicate maintenance.

Color system audit

Migrating existing variables and Sass functions to Relative Color Syntax

Design tokens

Building a scalable token architecture with oklch and Relative Color Syntax

Multi tenant theming

Only one base color per customer, all shades computed automatically

10. Summary

Relative Color Syntax changes how color systems get built in CSS. Instead of maintaining a separate custom property for every variant of a color, a single base color is used, and everything else gets computed at runtime in the browser with from, color space channels and calc(). Combined with oklch(), the result is perceptually uniform shading that works equally well for any brand color, with no manual tuning per hue.

The biggest practical win shows up in design systems and multi tenant applications, where a single variable per brand automatically produces complete palettes including hover, active and transparent states. With an @supports fallback, Relative Color Syntax can already be used in production today, while older browsers receive a static reserve color.

Relative Color Syntax in Practice — The Essentials at a Glance

Basic syntax

oklch(from var(--brand) l c h) reads the channels of any color and allows targeted adjustment of individual values.

Combine with oklch

A perceptually uniform lightness axis makes shading formulas independent of the actual hue.

Alpha channel

Derive transparent variants directly from the base color, with no second variable in rgba notation.

Fallback

@supports (color: oklch(from red l c h)) gives older browsers a static reserve color.

11. FAQ: Relative Color Syntax in Practice

1What is Relative Color Syntax?
A CSS Color Level 5 feature that, using from, lets you read the channels of an existing color and change them with calc().
2Better than Sass darken/lighten?
Sass produces static build time values. Relative Color Syntax computes live in the browser and reacts to custom property changes.
3What unit does l use in calc()?
Normalized to 0 through 1. calc(l - 0.08) lowers lightness by eight hundredths, not eight percentage points.
4Does it work with hsl or rgb?
Yes, any target color space works. oklch is recommended for shading because of its perceptually uniform axis.
5How do I check support?
@supports (color: oklch(from red l c h)) specifically checks the Relative Color Syntax capability, not just oklch in general.
6Are transparent variants possible?
Yes, through alpha in the expression, for example l c h / 0.25, with no second variable in rgba notation needed.
7Good for multi tenant theming?
Very good, one base variable per tenant is enough, all shades compute automatically from one shared formula.
8Does it fully replace color-mix?
No, color-mix blends two colors, Relative Color Syntax changes channels of a single color. Both are often used together.
9Do I need to remove Sass?
No, a gradual switch is enough. Build new color functions natively, existing Sass calculations keep running in parallel.
10Which browsers support it in 2026?
Chrome, Edge and Safari fully since 2023/2024, Firefox since version 128. Fallback via @supports for older versions.