Hue-angle transitions

Update 2014-06-25

I created a library called sweep.js that addresses this problem.

Edit 2018-01-22

HUSL has been renamed to HSLuv. References have been updated.

Update 2023-05-28

Many problems described in this post can now be solved using color-interpolation-method.

I was recently browsing a beautiful red monochrome1 site. There were a few call-to-action buttons on the page that transitioned to green on hover.

Where did that brown come from?! The initial red looks great, the final green looks great, but everything between is pretty dismal. Why is that?

The RGB color model

Red Green and Blue are the three primary additive colors. Your browser takes an 8-bit intensity value for each of the R, G, and B components to determine the output color. This is often represented as three two-digit hex values (for example, #ff8800 is orange).

Three-dimensional RGB color space
Axes represent red, green, and blue

For the subtle changes that make up 90% of color transitions, moving through RGB space looks fine since it ensures steady linear transitions between two similar endpoints. The problem with RGB is that when we start trying bolder transitions we must traverse a muddy grey no man’s land in the center of the space; for example, imagine connecting yellow and blue in the cube image above.

The HSL representation

HSL stands for Hue, Saturation, Lightness. It maps RGB’s cartesian space to cylindrical coordinates, which means that hue is represented as an angle around the RGB color wheel. It’s a friendlier representation to use since changes can be more explicit. For example, to make an intense red more subdued, just reduce saturation from 100% to 50%. For comparison, doing this in RGB requires reducing R from 255 to 191 and increasing G and B from 0 to 64.

Three-dimensional HSL color space
Axes represent hue, saturation, and lightness

Passing RGB values lets you manipulate pixels as though you were mixing paint. HSL abstracts this process so that you’re describing the color that you want rather than the colors that make up the color that you want.

Transitioning through HSL

Since HSL takes hue as one of its arguments, you’d think that transitioning from one hue to another would be a breeze. Sadly, browsers convert all HSL colors to their RGB counterparts before using them, so transitions between RGB and HSL end up looking the exact same.

We can do better than this, but we’re going to have to use some JavaScript. There has been discussion of changing the way HSL transitions work in the browser, but nothing’s come of it yet. For now, let’s run a little comparison that updates both RGB and HSL values at each step.

The HSL transition maintains a consistent saturation, so we’re able to avoid the sludge of RGB. Unfortunately, we’re still left with an awkward transition. Our HSL version peaks in lightness at yellow, leaving a jarring shift between yellow and the final color. Though the colors are nicer through HSL, the RGB version is still more appealing since it has natural apparent timing.

We can come up with a localized easing function to make the HSL transition smoother. By mapping the trough of a sine curve to the peak at the center of our transition, we can move slowly through the area where change is greatest:

The adjusted version looks better, but it’s still not great. A smooth transition could be reached after extensive tweaking, but our timing function would only be relevant for a red-to-green transition; any other combinations would require their own custom timing functions.

The unbearable lightness of being

The real issue here is that HSL does a bad job dealing with perceived lightness. Here’s the HSL spectrum with 50% lightness:

Two-dimensional HSL spectrum

And here’s the associated lightness values based on CIE’s definition of lightness:

Perceived lightness of a two-dimensional HSL spectrum

This is why color pairs require custom timing functions. Our eyes try to find a pattern when they notice changes in lightness. In RGB, we get consistently lighter or darker, so the linear shift is easy for our eyes to comprehend. In HSL, we ride a wonky, inconsistent wave of light/dark patches until we arrive at our destination. Since our eyes can’t discern a pattern, the transition between color-groups feels springy.2

HSLuv to the rescue?

Plenty of discussions on the inconsistencies of HSL lightness already exist, so I’ve thrown some of my favorite links into the references. One particularly awesome project is HSLuv, which uses CIE’s definition of lightness to smooth out the space. In HSLuv, large-angle transitions appear less jumpy; check it out!

HSLuv gets us very close to perceptually linear hue-angle transitions. It also puts a ceiling on our saturation, dragging us closer to the sludgy colors that we’ve been avoiding all along. Some transitions like cyan to violet end up looking awesome. Others, such as our original example of red to green, look far worse than they would in RGB or HSL.

We’ve come all this way, we’ve systematically solved all of our problems with the initial transition, and we still ended up with something worse than what we started with. What gives?


It turns out that there’s no one best way to transition between colors. Sometimes we need overlaying text to remain readable, so consistency of lightness is the primary concern. Sometimes, we just want consistently pretty colors. A summary of the benefits and shortcomings found is shown below:

Browser supportBestOkayWorst
Lightness consistencyOkayWorstBest
Linear timingBestWorstOkay
Pretty intermediate colorsWorstBestOkay
True colorsBestBestWorst

It’s all contextual and boils down to choosing a method that’s suited to your application. Use HSLuv for text containers to ensure that everything remains readable. Use HSL when you need to transition from grey to full-juice saturation. For a design-heavy red website, maybe the real answer is to forgo red-green transitions altogether and choose a different link effect.

Remember, since browsers only support transitions through RGB right now they should be your go-to on the web for all but the most special cases. I hope that the spec will start supporting hue-angle transitions as the default between two HSL colors, but it’s a long way away. For now, be conscious of the color paths that your transitions are taking. If you don’t like them, experiment!



  1. Okay, so it was monochrome until you hovered.

  2. This is one reason why those “party LEDs” in pools and restaurants have such unsubtle hue-shifting.