Colors are Math: How they match — and how to build a Color Picker
Tags: css, javascript, design, webdev
Published: 2021-03-20 16:33:18

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](https://dev.to/madsstoumann/creating-a-fun-fast-secure-and-sustainable-website-50nk).

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 **H**ue **S**aturation and **L**ightness.

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

![ColorWheel](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3qul2upbi8ko7vdg8bm7.png)

In CSS, it's:

```css
.wheel {
  background: conic-gradient(
    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%)
  );
  border-radius: 50%;
}
```

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

![LinearGradient](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rl9orndcqpb8n9y62e1y.png)

---

## Matching Colors
When colors look great together, it's all down to how they [relate to eachother](https://www.tigercolor.com/color-lab/color-theory/color-harmonies.htm) 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](https://en.wikipedia.org/wiki/Complementary_colors) 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:

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

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

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

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

```css
.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:

```css
.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°):

```css
.triad1 {
  hsl(calc(var(--h) + 120), var(--s), var(--l));
}
.triad2 {
  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°:

```css
.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°:

```css
.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:

```css
.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
Shades removes light from the `--l` – lightness:

```css
.shade10 {
  hsl(var(--h), var(--s), calc(var(--l) - ((100% - var(--l)) / 10) * 1));
}
.shade20 {
  --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:

![ColorTheme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fk4emdc243rxps0whypl.png)

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`:

```html
<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">`:

```css
.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:

```js
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](https://codepen.io/stoumann/pen/vYXmBpW):

{% codepen https://codepen.io/stoumann/pen/vYXmBpW %}

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

![ColorEditor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/agyptl04lpna80hxsmmr.png)

It also features a **gradient**-editor, for all types of CSS gradients: conic, linear and radial:

![Gradient](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jvwtngd1z00f4a79pp3v.png)

You can find the tool on Codepen:
{% codepen https://codepen.io/stoumann/pen/QWKdKxJ %}

---

## Bonus: HSB Color Picker

![Photoshop](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4bo53h0424rsehr8qwae.png)

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 **L**ightness 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:

```html
<div class="hue">
  <div class="brightness"></div>
  <div class="saturation"></div>
</div>
```

```css
.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:

![SaturationBrightness](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l6uhwmefsqcybm5gght6.png)
 
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!

Thanks for reading!