Better Color Gradients with HSLuv
Color is tricky! It's often more complicated than I expect; when I underestimate it, I usually end up taking a dive down into some serious color theory and math. Let's take a closer look at an interesting challenge when generating color ramps and gradients.
CSS3 introduced the linear-gradient()
function that lets you create a smooth transition between multiple colors. It makes it easy to pop a gradient here and there in CSS. For example:
A simple gradient from yellow to red. Neat! And it's all grape!
Except…
The linear-gradient()
function uses a simple RGB interpolation to blend between colors. It's super fast, but it can create less than ideal mix of colors. Especially when the two colors are on the opposite side of the color wheel from each other. Consider:
Which looks a little like this:
This might not be what you expect. Depending on what you are doing, you might be asking yourself, What's up with that weird color in the middle?!
Let's take a closer look at the math that causes this.
Mixing in Rgb Space
The color mixing algorithm breaks down the start and end color into its red, green and blue color components and mixes them.
First let's break down the colors:
CSS Color | Hex | Red | Green | Blue |
---|---|---|---|---|
yellow | #FFFF00 | 255 | 255 | 0 |
blue | #0000FF | 0 | 0 | 255 |
Then, we can define a simple linear tween:
Now, look what happens when we get to the halfway point:
We get a nice middle gray. Yummy…
So, How Can We Fix It?
While there is no way define a different color interpolation algorithm for linear-gradient()
, but we can come with bit of a compromise.
Instead of using linear RGB component tweens, we can use another color space. Then sample the output of this other algorithm, and feed that into our RGB tween and get similar results.
Let's try HSL. HSL is a cylindrical-coordinate system for defining color. It's composed of three parts:
- Hue
- Saturation
- and Lightness.
Hue is usually represented as degrees; Saturation and Lightness as percentages. Let's turn our target colors into HSL rather than RGB:
CSS Color | Hue | Saturation | Lightness |
---|---|---|---|
yellow | 60° | 100% | 50% |
blue | 240° | 100% | 50% |
Now, to interpolate between those two color, we need to make a new kind of tween. We need a circular tween:
Now, let's tween it up. I'm going to generate seven stops in my tween, and use tinycolor.js to convert from HSL back into hex colors:
And here is what we get out:
Now, let's plug those into a linear-gradient()
; this will linearly interpolate between the stops, but will be closer to a true HSL interpolation.
Well, we don't get that weird gray anymore, but it's a little … colorful.
But, We Can Do Even Better!
You may be tempted to stop here and move on, after all you can define hsl()
colors directly in CSS. But there is still some room for improvement.
HSL doesn't take into account the perceptual brightness of a color. Greens look brighter than blues, even though they have the same Saturation and Lightness. Other color spaces have attempted to account for human perception in their color model. But they are complicated… But let's use HSLuv instead!
HSLuv is a self described as …a human-friendly alternative to HSL.
It does a better job of maintaining perceptual brightness between relative hues… Let's rerun our tween, but instead of HSL we'll use HSLuv.
And here is what we get out:
Quick, let's make a gradient!
A little smoother; and doesn't have a bright peak at cyan! I'm going to call that a win!
Wrapping Up
Remember, a simple linear color gradient might not be the best choice for what you are trying to make.