Posted on

# Colors are Math: How they match — and how to build a Color Picker

Why do some color-combinations look better than others? Spoiler: it involves math.

The other day, I wrote about the Color Theme Tool on my new website.

This tool, along with some other color-tools, I'll introduce in this post, are all based on the HSL color-format.

HSL stands for Hue Saturation and Lightness.

Hue is the main color — in degrees.
If you look at the color-wheel, it's a series of colors, in 30° intervals:

In CSS, it's:

``````.wheel {
hsl(0, 100%, 50%),
hsl(30, 100%, 50%),
hsl(60, 100%, 50%),
hsl(90, 100%, 50%),
hsl(120, 100%, 50%),
hsl(150, 100%, 50%),
hsl(180, 100%, 50%),
hsl(210, 100%, 50%),
hsl(240, 100%, 50%),
hsl(270, 100%, 50%),
hsl(300, 100%, 50%),
hsl(330, 100%, 50%),
hsl(360, 100%, 50%)
);
}
``````

To turn it into a horizontal or vertical slider, change the gradient-type to `linear-gradient`:

## Matching Colors

When colors look great together, it's all down to how they relate to eachother in the color-circle.

When you've selected a color (which we'll call the primary color), the color directly opposite that color (180° degrees), is called the complimentary color – and these two colors always look great together.

Let's split our HSL up into 3 CSS Custom Properties:

`--h`, `--s` and `--l`.

– and look at how we can use simple math to `calc` colors, that match our primary color:

``````.primary {
hsl(var(--h), var(--s), var(--l));
}
``````

The complimentary color adds 180° to `--h`:

``````.complimentary {
hsl(calc(var(--h) + 180), var(--s), var(--l));
}
``````

The Split Complimentary colors are 150° and 210° from the primary color:

``````.splitcom1 {
hsl(calc(var(--h) + 150), var(--s), var(--l));
}
.splitcom1 {
hsl(calc(var(--h) + 210), var(--s), var(--l));
}
``````

Analogous colors are the colors next to the selected color (both sides) – in this case our primary color:

``````.analogous1 {
hsl(calc(var(--h) + 30), var(--s), var(--l));
}
.analogous2 {
hsl(calc(var(--h) - 30), var(--s), var(--l));
}
``````

Triadic colors are evenly spaced around the color wheel, so from our primary color, its 120° and 240° (or: minus 120°):

``````.triad1 {
hsl(calc(var(--h) + 120), var(--s), var(--l));
}
hsl(calc(var(--h) - 120), var(--s), var(--l));
}
``````

The Square Colors consist of our primary color, and colors at 90°, 180° (complimentary) and 270°:

``````.square1 {
hsl(calc(var(--h) + 90), var(--s), var(--l));
}
.square2 {
hsl(calc(var(--h) + 270), var(--s), var(--l));
}
``````

The Tetradic Rectangle is similar to the square, and consists of colors at 60°, 180° (complimentary) and 240°:

``````.tetra1 {
hsl(calc(var(--h) + 60), var(--s), var(--l));
}
.tetra2 {
hsl(calc(var(--h) + 240), var(--s), var(--l));
}
``````

### Tints

Tints add light to the `--l` – lightness:

``````.tint10 {
hsl(var(--h), var(--s), calc(var(--l) + ((100% - var(--l)) / 10) * 1));}
.tint20 {
hsl(var(--h), var(--s), calc(var(--l) + ((100% - var(--l)) / 10) * 2));
}
/* etc. */
``````

Shades removes light from the `--l` – lightness:

``````.shade10 {
hsl(var(--h), var(--s), calc(var(--l) - ((100% - var(--l)) / 10) * 1));
}
--c-sh2: hsl(var(--h), var(--s), calc(var(--l) - ((100% - var(--l)) / 10) * 2));
}
/* etc. */
``````

All these CSS `calc`ulated -colors, are what I'm using in my CSS Color Theme Tool, based on the `--h`, `--s` and `--l` properties:

Now, let's look at how to build a Color Picker.

## HSL Color Picker

All it takes to create a simple, HSL-based Color Picker, is three `<input type="range">`-controls, one for each of the CSS Custom Properties: `--h`, `--s` and `--l`:

``````<form id="hsl">
<input type="range" name="--h" min="0" max="360" value="0" />
<input type="range" name="--s" min="0" max="100" value="100" data-suffix="%" />
<input type="range" name="--l" min="0" max="100" value="50" data-suffix="%" />
<div class="hsl"></div>
</form>
``````

In CSS, style the sliders as you want, and assign the calculated HSL-color to the color preview, the `<div class="hsl">`:

``````.hsl {
aspect-ratio: 1/1;
background-color: hsl(var(--h,0), var(--s,100%), var(--l, 50%));
width: 20rem;
}
``````

Finally, in JavaScript, add a single eventListener on the form, that'll update the CSS Custom Properties:

``````hsl.addEventListener('input', (event) => {
const input = event.target;
document.documentElement.style.setProperty(input.name, `\${input.valueAsNumber}\${input.dataset.suffix||''}`)
})
``````

And that's it! I used this method (and a bit more JavaScript), to create these small color pickers:

A bit more complex, this Color Tool also use HSL-based sliders, and JavaScript to convert between the various formats (rgb, cmyk, hex):

You can find the tool on Codepen:

## Bonus: HSB Color Picker

The main area in Photoshop's Color Picker, is made of three layers, and is way easier to understand, if you look at the HSB-format.

The B is for Brightness, and use a slightly different algorithm than the Lightness of HSL.

If you look at the HSB-area as a coordinate-system, Saturation is the x-axis (left-ro-right, 0 to 100) and Brightness is the y-axis (bottom-to-top, 0 to 100).

Thus, the top-right position is `100, 100` and the bottom-left `0, 0`.

To re-create this in HTML and CSS, use three layers:

``````<div class="hue">
<div class="brightness"></div>
<div class="saturation"></div>
</div>
``````
``````.hue {
--h: 0;
background-color: hsl(var(--h), 100%, 50%);
}

.saturation {
background-image: linear-gradient(to bottom, transparent, hsl(var(--h), 0%, 0%));
}

.brightness {
background-image: linear-gradient(to right, hsl(var(--h), 100%, 100%), transparent);
}
``````

The `.saturation` and `.brightness`-layers need to be positioned `absolute`, relative to the `hue`-layer.

The CSS does not reflect the actual saturation and brightness, but layered like this, the illusion is complete:

Try changing the value of `--h`.

If you want to build a Photoshop-like color-picker in JavaScript, detect the x and y-position of the pointer-device, use `getBoundingClientRect()` to get the dimensions of the »picker area«, and convert it, so you'll always get a coordinate between `0,0` and `100,100`.

Again: `x` equals Saturation and `y` equals Brightness.

You'll also have to convert HSB to HSL(with JavaScript), as browsers don't understand the HSB-format.

So ... I think HSL-based color-pickers are easier to both code and use!

I wish there was degree markings on the color wheel at 120°/180° and hue picker strip

Good idea!

Samuel Karani

looks good

Matvey Romanov

Pretty useful, thanks

Thanks!

iFTekhar

Awesome keep it up.🔥

Thank you!

Jay Jeckel

Awesome article, very thorough.

Thank you!

Fun article!

I was happy to see the Photoshop reference. :-)

Thanks!

Kevin Hicks

This is an interesting approach on how to pick matching colors. I've never heard of it described this way and it's helpful for someone like me who is more technical than creative.

I like how we can use math to calculate colors, and physics (wavelengths and eV) to determine the energy consumed by different colors on our devices!

Jeferson Brito

wow that's impressive!

Thanks!

Gaurav

This is amazing!