Troubleshooting Cross-Browser Compatibility with CSS Relative Color Specs

Lorenzo Migliorero
4 min readNov 25, 2024

--

The new CSS Relative Color APIs are amazing.
Thinking that this would become part of an official CSS Spec a couple of years ago was impossible.

However, while developing my new personal website, I encountered an intriguing case of cross-browser compatibility related to it, specifically within the LCH spectrum but probably also valid for HSL.

The Goal

Starting with a base color, I needed to create two additional colors with slightly rotated hues to form a radial gradient applied on the body when the user selects a list element:

The build module from my personal website lorenzomigliorero.com

A perfect scenario for the LCH color spectrum.
Using the LCH color space, we can define a new color from the variable var( — base) rotating the hue by -50 degrees:

lch(from var(--base) l c calc(h - 50))

The Problem

The problem, however, lies in how this color is computed across different browsers.

Here’s a concrete example:

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(from var(--base) l c calc(h - 100));
--color-2: lch(from var(--base) l c calc(h + 100));
}

Let’s compare the computed colors across browsers:

/* Chrome 131 */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.6731 26.5719 96.461 / 0.74902);
--color-2: lch(37.6731 26.5719 296.461 / 0.74902)
}

/* Edge 129 */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.6731 26.5719 96.461 / 0.74902);
--color-2: lch(37.6731 26.5719 296.461 / 0.74902)
}

/* Firefox 132 */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.6775 26.5805 96.4548 / 0.75);
--color-2: lch(37.6775 26.5805 296.455 / 0.75)
}

/* Safari 18.1 */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.677467 26.580475 96.454803 / 0.75);
--color-2: lch(37.677467 26.580475 296.454803 / 0.75)
}

/* Safari 17 */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: rgba(0,0,0,0);
--color-2: rgba(0,0,0,0)
}

Except for minor rounding differences, all the modern browsers computed the same values except for Safari 17, which is still widely used today. After looking at the specs, it turned out it implements an older version of the Relative Color LCH spec, which requires hue values explicitly expressed in degrees when performing calculations.

The Fix

To handle this issue, I included a @supports rule to define a — hue-unit variable that dynamically assigns the correct unit (1deg) for browsers still relying on the old specification:

:root {
--hue-unit: 1;
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(from var(--base) l c calc(h - 100 * var(--hue-unit)));
--color-2: lch(from var(--base) l c calc(h + 100 * var(--hue-unit)));
}

@supports (color: lch(from black l c calc(h - 1deg))) {
:root {
--hue-unit: 1deg;
}
}

Let’s re-run the tests:

/* Chrome */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.6731 26.5719 96.461 / 0.74902);
--color-2: lch(37.6731 26.5719 296.461 / 0.74902)
}

/* Edge */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.6731 26.5719 96.461 / 0.74902);
--color-2: lch(37.6731 26.5719 296.461 / 0.74902)
}

/* Firefox */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.6775 26.5805 96.4548 / 0.75);
--color-2: lch(37.6775 26.5805 296.455 / 0.75)
}

/* Safari 18.1 */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.677467 26.580475 96.454803 / 0.75);
--color-2: lch(37.677467 26.580475 296.454803 / 0.75)
}

/* Safari 17 */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.677925 26.58409 96.4662);
--color-2: lch(37.677925 26.58409 296.4662)
}

Problem solved! Or so I thought.

Another Safari 17 Quirk

While Safari 17 could now correctly interpret the declaration, it ignored the alpha channel unless explicitly defined. After a more detailed look at the specs, I found the alpha keyword, which preserves the original alpha value:

:root {
--hue-unit: 1;
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(from var(--base) l c calc(h - 100 * var(--hue-unit)) / alpha);
--color-2: lch(from var(--base) l c calc(h + 100 * var(--hue-unit)) / alpha);
}

Success! Finally, the colors look the same across all browsers.

/* Chrome */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.6731 26.5719 96.461 / 0.74902);
--color-2: lch(37.6731 26.5719 296.461 / 0.74902)
}

/* Edge */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.6731 26.5719 96.461 / 0.74902);
--color-2: lch(37.6731 26.5719 296.461 / 0.74902)
}

/* Firefox */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.6775 26.5805 96.4548 / 0.75);
--color-2: lch(37.6775 26.5805 296.455 / 0.75)
}

/* Safari 18.1 */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.677467 26.580475 96.454803 / 0.75);
--color-2: lch(37.677467 26.580475 296.454803 / 0.75)
}

/* Safari 17 */

:root {
--base: rgba(0, 100, 100, 0.75);
--color-1: lch(37.677925 26.58409 96.4662 / 0.7490196);
--color-2: lch(37.677925 26.58409 296.4662/ 0.7490196)
}

Conclusion

The CSS Relative Color APIs are incredible, and I’ve only scratched the surface of their potential. However, we’ll likely need to support the old spec for several months, and I hope this article provides helpful guidance on this topic!

Codepen used for testing here:

--

--

Lorenzo Migliorero
Lorenzo Migliorero

Written by Lorenzo Migliorero

Senior Frontend Engineer based in Amsterdam dedicated to crafting clean, accessible, and visually engaging user interfaces.

No responses yet