DEV Community

Cover image for Colors are Math: How they match — and how to build a Color Picker
Mads Stoumann
Mads Stoumann

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:

ColorWheel

In CSS, it's:

.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%;
}
Enter fullscreen mode Exit fullscreen mode

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

LinearGradient


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));
}
Enter fullscreen mode Exit fullscreen mode

The complimentary color adds 180° to --h:

.complimentary {
  hsl(calc(var(--h) + 180), var(--s), var(--l));
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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));
}
.triad2 {
  hsl(calc(var(--h) - 120), var(--s), var(--l));
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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. */
Enter fullscreen mode Exit fullscreen mode

Shades

Shades removes light from the --l – lightness:

.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. */
Enter fullscreen mode Exit fullscreen mode

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

ColorTheme

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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||''}`)
})
Enter fullscreen mode Exit fullscreen mode

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

ColorEditor

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

Gradient

You can find the tool on Codepen:


Bonus: HSB Color Picker

Photoshop

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>
Enter fullscreen mode Exit fullscreen mode
.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);
}
Enter fullscreen mode Exit fullscreen mode

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

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!

Oldest comments (17)

Collapse
 
ra1nbow1 profile image
Matvey Romanov

Pretty useful, thanks

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

Collapse
 
jayjeckel profile image
Jay Jeckel

Awesome article, very thorough.

Collapse
 
madsstoumann profile image
Mads Stoumann

Thank you!

Collapse
 
devggaurav profile image
Gaurav

This is amazing!

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

Collapse
 
iftekhs profile image
iFTekhar

Awesome keep it up.🔥

Collapse
 
madsstoumann profile image
Mads Stoumann

Thank you!

Collapse
 
kevinhickssw profile image
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.

Collapse
 
madsstoumann profile image
Mads Stoumann

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!

Collapse
 
eljayadobe profile image
Eljay-Adobe

Fun article!

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

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

Collapse
 
chiefmikek profile image
GSMC Mike A. Kouklis USN-Retired

15x24_div+screenshot.png (1280×572)
I wish there was degree markings on the color wheel at 120°/180° and hue picker strip

Collapse
 
madsstoumann profile image
Mads Stoumann

Good idea!

Collapse
 
samuelkarani profile image
Samuel Karani

looks good

Collapse
 
jeferson_sb profile image
Jeferson Brito

wow that's impressive!

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!