Animating a Hue around the Color Wheel with Houdini

Even though the hsl() color function allows us to specify hue, saturation, and lightness levels individually, there is no straightforward way to animate one value independently of the others. We will explore how color interpolation works on the web, look at our options to animate a hue around a color wheel, and see how Houdini gives us a mechanism to do this.

The basics of HSL

The beauty of defining a color with hsl() is that you can start with a hue and then tweak it. If you want a reddish color you use a hue near 0, or a cyan color is on the opposite side of the wheel at 180 (the values represent degrees in a circle, so the range of different values are from 0 to 360). The other two values are percentages from 0% to 100%. Saturation shows how full the color is at 100% or grayed out more as you approach 0%. Lightness at 0% is always black, 100% is always white, and 50% is a good default for getting a bright color.

See the Pen Visual Reference: Colors via HSL by Dan Wilson (@danwilson) on CodePen.

Color Interpolation

Interpolation is effectively how the values between a start and end state are determined during an animation. Browsers are very efficient at determining the values between two numbers such as filling in an opacity animation from 0 to 1.

Determining what colors to show between two color states in an animation is actually also straightforward for a browser to accomplish, as colors boil down to a set of numbers.

With RGB colors like #ffaabb and rgb(255,170,187) we have three sets of numbers that can be interpolated individually to determing a color value at each step in the animation. So if we want to move from a magenta (rgb(255,0,255)) to a cyan (rgb(0,255,255)) the R value will animate from 255 to 0, the G value will go from 0 to 255, while the B value stays at 255. This process is also how the inbetween values are determined for gradients.

See the Pen Color Interpolation by Dan Wilson (@danwilson) on CodePen.

Every step of this transition is still very much a blend of these two colors. With hsl() we get something a little more interesting where you can change a single value (the hue) and potentially get every color of the rainbow. Since we were able to interpolate each of the three RGB parts, you might think the three individual parts of the hsl() will interpolate on their own as well and we could actually rotate around the color wheel simply by animating from hsl(0,100%,50%) to hsl(360,100%,50%).

See the Pen Animating from red to red by Dan Wilson (@danwilson) on CodePen.

This red block is technically animating, I promise. However, regardless how you define your color, CSS always animates between its computed values in RGB in the sRGB color space. So even though we used HSL in the previous example, it technically is doing an animation from #ff0000 to #ff0000. You can edit the example to instead go from a hue of 0 to a hue of 180 (cyan) to see it doing the same animation you would find by going red to cyan or #ff0000 to #00ffff.

So whether you use named colors, RGB, HSL, or future methods like hwb(), the color stops are effectively converted to RGB and then have the interpolation applied.

Is it possible to animate a hue change only?

CSS Filters and hue-rotate

It is! Sort of!

CSS Filters have been well supported for several years now across the board, including non-Chromified-Edge. Most people tend to lean on filters for the blur() function, but there are several other options including hue-rotate().

This will take the current hue of your color and rotate it around the color wheel the angle of degrees you specify. So let’s try our animation again with a starting hsl(0,100%,50%) and animate the hue to 360 via hue-rotate(360deg).

See the Pen Animating from red to red (hue-rotate) by Dan Wilson (@danwilson) on CodePen.

We did it!*

With an asterisk!

My goal here was to keep the saturation and lightness consistent and then change only the hue from 0 to 360. The hue was accurately rotated around a color wheel, but if you look closely at the color changes, we should see colors very similar to the perimeter of the circle in the first example of this article, where we can very clearly see each bright color from a rainbow — red, orange, yellow, green, blue, indigo, and violet. This animation clearly has a red, green, and blue, but everything in between is much darker and muddier than I’d expect.

That is because hue-rotate() does not use the same color wheel as you get with hsl() (as Amelia Bellamy-Royds wonderfully explains in this blog comment). It gets complicated with color spaces and the fact that hue-rotate uses a combination of hue, saturation, and luminance instead of lightness.

See the Pen What I Want out of hue-rotate by Dan Wilson (@danwilson) on CodePen.

So this gets us a more interesting color animation, but it is not a rainbow animation at the same saturation and lightness. We can approximate a true hue rotation by using multiple keyframes with different colors. For example we get a little closer if we set an animation with three states of hsl(0,100%,50%), hsl(120,100%,50%), hsl(240,100%,50%). We get even closer as we do five keyframes and ten keyframes. With 36 frames we get an approximation that is very close to the desired result.

See the Pen HSL: Options to Rotate H with Same SL by Dan Wilson (@danwilson) on CodePen.

The Houdini Rotate

If you are in a browser that supports the Houdini Properties and Values API specification, you will see an extra animation in the previous example (try a Blink browser with Experimental Web Platform Features flag enabled or the latest Safari Technology Preview). That gives us the actual behavior I desired as it accurately keeps the saturation and lightness in place and interpolates the hue value between 0 and 360.

As we can use Custom Properties to individualize the parts of a value, we can work with a base hsl(var(--hue),100%,50%) color and modify our --hue to go from 0 to 360. Custom Properties themselves are not animatable out of the box, but Houdini allows the web author to provide a syntax for a specific custom property which tells it the rules to interpolate the property’s value.

With the following CSS, we are changing the hue from 0 to 360. The JavaScript is telling the browser to treat our custom property --hue as a number, and so it can follow the interpolation rules for numbers.

div {
  --hue: 0;
  background: hsl(var(--hue), 100%, 50%);
  animation: hue-rotate 10000ms infinite linear;
}
@keyframes hue-rotate {
  100% {
    --hue: 360
  }
}
if (window.CSS && CSS.registerProperty) {
  CSS.registerProperty({
    name: '--hue',
    syntax: '<number>',
    inherits: false,
    initialValue: 0
  });
}

And with that we get a true hue rotation that maintains the same saturation and lightness found in our hsl() root color. We can similarly animate along saturation and lightness by setting up custom properties for each and registering the properties in JavaScript with the <percentage> syntax and an initial value that is a percentage, such as 100%.

See the Pen HSL Houdini by Dan Wilson (@danwilson) on CodePen.