Fork me on GitHub


Human-friendly HSL




What is HUSL?

HUSL is a human-friendly alternative to the HSL color space. HSL was designed back in the 70s to be computationally cheap. It is a clever geometric transformation of the RGB color space and it does not take into account the complexities of human color vision.

There have long existed color spaces designed for perceptual uniformity. One of these color spaces is CIELUV (and its cylindrically shaped brother CIE LChuv). Like HSL, it defines hue and lightness, but instead of saturation it defines chroma. The problem with its chroma component is that it doesn't fit into a specific range. This makes it very hard to define colors programmatically. HUSL is a modified version of the CIE LChuv color space with a new saturation component.

Lightness Relative Absolute Absolute Absolute
Saturation* Relative Absolute Relative Absolute
Hue uniformity Poor Good Good Good
Component ranges Defined Undefined Defined Defined
Saturated colors Yes Yes Yes No
* Saturation and chroma are actually distinct concepts


The chroma component of CIE LChuv is absolute. Unlike HSL's saturation you can effectively use it to compare two different colors. The dips in the graph represent impossible colors (such as dark saturated yellow). CIE LChuv doesn't warn you about them, making it unsuitable for generating colors.



HUSL preserves the lightness and hue components of CIE LChuv and stretches its chroma vertically so that every color has the same range, defined as a percentage.


HUSLp takes as many colors as it can from CIE LChuv without distorting the chroma. As you can see, the resulting color space is smooth, but only "pastel" colors can be included.


Both HSL and HUSL have a distorted chroma map, this is a trade-off of their convenient shape. HUSL's chroma has more sudden shifts, perhaps it can be improved with a better scaling function. Fork my code!


Have you ever tried to use HSL to work out contrast? Define background and text colors? I know I have, with horrible results.

This is why. Two different colors with the same lightness as defined by HSL can actually have very different lightness. For this demo I am using CIE's definition of lightness. Both CIE LChuv and HUSL use this component, so their picture would be an even grey. This definition of lightness is based on experimental results, you will find that it works very well.

Let's try to generate some random background colors:

function randomHue() {
  return Math.floor(Math.random() * 360);
// HUSL demo
$(...).css('background-color', $.husl.husl(randomHue(), 90, 60));
// HSL demo
$(...).css('background-color', hslToHex(randomHue(), 90, 60));
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%


HUSL's uniform hue means random colors will be truly random. Iterating over colors will also produce better results.

// HUSL demo
$('#rainbow-husl div').each(function(index) {
  $(this).css('background-color', $.husl.husl(index * 36, 90, 60));
// HSL demo
$('#rainbow-hsl div').each(function(index) {
  $(this).css('background-color', hslToHex(index * 36, 90, 60));




HUSL works well already, but welcomes revisions that improve upon it. The project uses semantic versioning, so any backwards-incompatible change will be marked clearly (by incrementing the x in x.y.z). I am not an expert on colors, so I greatly welcome anyone's insight. You can fork this project on GitHub or talk to me via email. The code is released under the MIT license, the math behind it is in the public domain.

Each revision is specified by the code of its reference implementation. This code is used to generate a snapshot of the color space. These snapshots, serialized in JSON, are used for regression testing. If you want to port HUSL to a different language or finish one of the started ports, you should match your implementation against the snapshot generated by the reference implementation. The snapshots are stored in the repo.

Original Revision 1 Revision 2
Reference implementation
v1.0.2 v2.0.0 v4.0.0
Python v2.1.0
Needs work
Needs work
Needs work

How does HUSL currently work?

HUSL is defined as a conversion to and from CIE LChuv. This conversion is a two step process.


  1. Find the maximum chroma for a given lightness and hue
  2. Scale the saturation to fit inside that chroma range (currently linear)

Finding the maximum chroma involves building three functions that take CIE LChuv's components as input and return RGB channel values, one for each channel. We equate each to 1 and 0, and solve the 6 resulting equations for C. When we plot the 6 solutions with a fixed lightness and hue for x, we get the picture on the right. As expected, it looks like CIE LChuv.


  1. Find the hue with the lowest maximum chroma for a given lightness
  2. Use that lowest maximum chroma point as a cut-off point; scale the resulting rectangle linearly

To find the extrema we differentiate the curves.

I used Maxima to build and solve these equations, and a wxMaxima file is bundled with the code.

Husl is not perfect

Lightness: 10%

Lightness: 95%


HUSL is written in CoffeeScript, you can use it server-side with npm install husl or client-side as a jQuery plugin ($.husl).

husl.toHex(hue, saturation, lightness)
hue is a number between 0 and 360, saturation and lightness are numbers between 0 and 100. This function returns the resulting color as a hex string.
husl.toRGB(hue, saturation, lightness)
Like above, but returns an array of 3 numbers between 0 and 1, for each RGB channel.
Takes a hex string and returns the HUSL color as defined above.
husl.fromRGB(red, green, blue)
Like above, but red, green and blue are passed as numbers between 0 and 1.

Use husl.p.toHex, husl.p.toRGB, husl.p.fromHex and husl.p.fromRGB for the "pastel" variant.

HUSL can also be used as a Stylus plugin. To use from command line, run stylus like this:

stylus < input.styl > output.css -u /path/to/husl.js

To use programmatically, do this:

var husl = require('husl');
  src: __dirname + '/app/views',
  dest: __dirname + '/public',
  compile: function(str, path) {
    return stylus(str)
      .set('filename', path);

Now you can use HUSL in your stylesheets!

  background-color husl(hue, 90, 80)
  color husl(hue, 90, 10)