React Color Picker

Most color pickers are steeped in the past with limited gamuts and colorspaces. Now that CSS Color Module 4 and higher-gamut colors are widely-available to users, color pickers need to update.

But not only that, in order to display colors in non-sRGB colorspaces, WebGL is needed to recreate colors accurately. Most colorpickers use CSS Gradients to simulate colors, which can achieve “good enough” accuracy for sRGB, but not for anything else.

FeatureTerrazzoreact-color@rc-component/color-pickerreact-colorfulreact-aria
WebGL
> 8bit depth
P3 Gamut
Rec2020 Gamut
OKLAB/OKLCH

Demo

%
%
%
%

Setup

npm i @terrazzo/react-color-picker @terrazzo/use-color
import ColorPicker from "@terrazzo/react-color-picker";
import useColor from "@terrazzo/use-color";

export default function MyComponent() {
  const [color, setColor] = useColor("#ff0000");

  return <ColorPicker color={color} setColor={setColor} />;
}

Methodology

The following decisions went into making Terrazzo’s color picker as good as it can be.

Sliders: accuracy

As pointed out in Björn Ottosen’s—the creator of OKLAB/OKLCH—fantastic article about color pickers, there’s a tension between accurate colorpickers being hard to use, while user-friendly colorpickers aren’t precise.

While his solution of an OKHSL colorpicker is a great compromise, it limits colorpicking to that colorspace alone. In order to be universally-usable for all colorspaces, per-channel sliders are needed for control.

Percentages: higher bit depth

Bit depth is different than gamut. A higher gamut color is more vibrant; a higher bit depth color is more precise.

Even people that have worked with RGB color a long time may not realize that integers 0-255 can’t support higher bit depth (in fact, 28, or 256, is what locks the colors into 8 bit depth).

This matters because in between, say, rgb(0, 0, 0) and rgb(1, 1, 1), users may have a monitor that can display rgb(0.5, 0.5, 0.5), but you won’t be able to access that color without expressing it as a percentage: rgb(0.2% 0.2% 0.2%). Why simply not use half the colors a user’s monitor can display, just because of your color tools?

warning

Hex codes are also locked into 8 bit depth, because they’re still the 28 colors just shortened into hexadecimal. Currently the only way to express higher bit depth is with percentages.

The Terrazzo color picker uses percentages so it’s future-proof and can express colors with any degree of accuracy needed for any monitor.

Toggles: gamut

Gamut triangles

sRGB, Display P3, and Rec 2020 (upcoming) gamuts in comparison. Each is significantly larger than the last. From Wikipedia.

By default, all colorspaces are clamped to the sRGB gamut, which is the smallest but is available for all users on all devices. The expanded Display P3 gamut—common but not quite 100% of all users and devices—has to be manually-enabled in the expanded options. The Rec2020 gamut, the largest one, isn’t yet available on the web, but it’s included for futureproofing this colorpicker for the day when it does become available.

Gamut toggles

Any gamuts that are toggled off won’t be reachable with the sliders. This ensures you’re not selecting colors outside the range of what’s intended.

API

Props

NameTypeDescription
colorColorA color object from @terrazzo/use-color
setColor(color: Color) => voidA setter function for the color object

use-color

The @terrazzo/use-color hook lets you memoize a color using Culori for fast, scientifically-accurate color operations.

import useColor, { formatCss } from "@terrazzo/use-color";

const [color, setColor] = useColor("#ff0000");

console.log(color.original); // { mode: "rgb", r: 1, g: 0, b: 0 }
console.log(color.css); // color(srgb 1 0 0)
console.log(color.p3); // { mode: "p3", r 0.9175, g: 0.2003, b: 0.1386 }
console.log(color.oklab); // { mode: "oklab", r: 0.628, g: 0.22, b: 0.13 }
console.log(color.oklch); // { mode: "oklch", r: 0.628, g: 0.257, b: 29.234 }

console.log(formatCss(color.oklab)); // color(oklab 0.628 0.22 0.13)

Colorspaces

The color object can convert to any CSS Color Module 4 colorspace using a memoized getter (meaning the work isn’t done until the colorspace is requested, and subequent requests are cached).

NameTypeDescription
color.originalobjectA Culori Color object
color.cssstringA CSS Color Module 4 color string
color.a98objectCulori Adobe RGB 98 object
color.hslobjectCulori HSL object
color.hsvobjectCulori HSV object
color.hwbobjectCulori HWB object
color.labobjectCulori CIEL*a*b* object
color.lchobjectCulori CIEL*c*h* object
color.lrgbobjectCulori Linear RGB object
color.okhslobjectCulori Okhsl object
color.okhsvobjectCulori Okhsv object
color.oklabobjectCulori Oklab object
color.oklchobjectCulori Oklch object
color.p3objectCulori P3 object
color.prophotoobjectCulori Protophoto RGB object
color.rec2020objectCulori Rec2020 RGB object
color.rgbobject(alias for srgb)
color.srgbobjectCulori sRGB object
color.xyzobject(alias for xyz65)
color.xyz50objectCulori XYZ D50 object
color.xyz65objectCulori XYZ D65 object

There’s also a formatCss() helper that’s similar to Culori’s but rounds numbers slightly (without losing accuracy) for a cleaner, easier-to-copy string.