Save Expand icon

Ron Valstar
front-end developer

MC Picker

I had a React project that needed a color picker. Actually I already use input[type=color] but the native implementations really suck (both on Windows and OSX). So I set out to find a minimal implementation. After about fifteen minutes I gave up. Most were setup too complex and I am pedantic when it comes to front-end code.

One element

That coloured panel for saturation and lightness that you see used everywhere (combined with one for hue) can be done with a single element (including circle selector). And the same goes for the hue panel.

Stacking gradients

The trick is layered CSS gradients. Some CSS properties like background and box-shadow can have multiple, comma-separates values. It is quite amazing what you can do with a single HTMLElement if you style it smartly.
We could even add the hue gradient into the saturation/lightness but it's easier with event listeners to let each have its own element.

So the gradient looks something like this:

.panel {
  background:
    linear-gradient(to top, black, rgba(0,0,0,0)
    ,linear-gradient(to left, red, white)
  ;
}

And with Javascript we can easily adjust any CSSRule especially if we add them ourselves:

document.body.appendChild(document.createElement('style'))
const sheet = document.styleSheets[document.styleSheets.length-1]
sheet.insertRule('.foo { color: red; }', 0)
const rule = sheet.rules[0]

Pseudo before and after

For quite some time now we can have two fake elements per real element controlled by the CSS pseudo selecors :before and :after. This is as close as we can currently get to shadow DOM.
Mind you these fake elements are inside the real element so you can never place them behind the real element because they are children, not siblings.
Since we only need event listeners on the main element the pseudo after is perfect for just reflecting the saturation/lightness position.

Altogether now

With that we can have a color picker that looks like this

<div class="mcpicker">
  <div></div>
  <div></div>
  <input>
  <input type="number">
  <input type="number">
  <input type="number">
</div>

...ehr, I mean this

Note that the box-shadow on the main element is also stacked, you can make your shadows look a lot more realistic that way.

Smaller and smaller and smaller

This makes the entire source of HTML/CSS/JS about 23KB (minified).
Which kind of bugged me because my unminified source was about 13KB. I was using a third party library called color-js for the actual color calculations. The library works perfectly but it's a bit large, and not only because it contains the entire list of CSS color strings. This is a pity if you only use a tiny portion, and the sources are setup in such a way that tree shaking fails.

Three dimensions

Rewriting the color calculations requires some understanding of the color models at play. The screen you are watching now uses the additive RGB model. The red, green and blue are actually teeny tiny light bulbs that, together, can display any color depending on the amount of light emitted.
This is straightforward enough but rather difficult and unintuïtive when it comes to visual representation in a user interface. If you put each color on it's own axis you get a simple three dimensional cube like this:

A 3D cube looks nice but what if you need a color from somewhere inside.
Which is why some people, a long long time ago devised a cunning plan to display this differently.

If we rotate the cube so that its black tip is at the bottom and its white tip is at the top we get this view.

Then we can force all the coloured vertices into the plane in between the black and white vertice, creating two hexagonal pyramids.

Instead of a red green and blue scale, we use color, intensity and lightness. Lightness is easy, it's the diagonal line from black to white in the cube. The color scale (or hue) is a bit more tricky: apart from rgb you also see the subtractive colors cyan, magenta and yellow (cmy). So the hue range is cbmryg, zigzagging along the cube. We can slant the cube so that black is on the bottom and white at the top, and pull all the hue colors into the same plane in between. Now the hue range has become a radial one and the intensity (or saturation or chroma) the distance of the radius to the vertical center.

You see that by rearranging the vertices we get different color models. But the color calculation is still a simple matter of linear interpolation.
I say simple but I did get the color conversion methods from somewhere, no need to reinvent the wheel for this.

By replacing and rewriting the color functions it is now down to 17KB minified.

So...

So that is that: a simple color picker implementation that works on any input[type=color] without any need to initialise. Just add script.