Part of UrStack, my open-source organization for TypeScript libraries. Siblings: UrVis, UrCharts, UrDock.
UrColor is a headless component library for building color interfaces. It ships the behaviour, meaning pointer math, keyboard handling, color conversion and ARIA wiring, and leaves every pixel of the presentation to the consumer.
Eight component families cover the vocabulary of color UI: area, slider, field, swatch, swatch picker, wheel, triangle, ring. Each one ships in all four bindings (Vue, React, Svelte, Angular) and works across 15 color spaces, from sRGB and HSL through OKLCH, Lab, Display P3 and CIE XYZ, on a zero-dependency CSS Color 4 engine.
Every color picker I reached for made the same trade. Either it looked exactly the way its authors wanted and fought every attempt to restyle it, or it was perceptually naive, interpolating gradients in sRGB and producing muddy ramps in modern color spaces.
The gap is structural, not accidental. React Aria and Reka UI both declare the
identical public type, ColorSpace = 'rgb' | 'hsl' | 'hsb', and Ark UI and
Zag.js share those same three. Every other picker on npm stores HSV or RGB and
converts on the way out. An OKLCH area, a Lab slider or a P3 gamut boundary
cannot be expressed by any of them, which is also why none of them need a GPU
renderer.
UrColor separates the two concerns. The library owns correctness and accessibility, the design system owns appearance.
sampleBilinearGrid, samplePolarGrid and friends) that
return Uint8ClampedArray RGBA ready for putImageData, which no other color
library ships@urcolor/core is benchmarked against culori, colorjs.io, chroma-js, colord,
tinycolor2 and @ctrl/tinycolor on every operation a picker performs: 59 groups,
Apple M1, Bun 1.3, mitata. It leads most of them.
| Operation | UrColor | Next fastest | Margin |
|---|---|---|---|
| Parse a hex color | 137 ns | colord, 138 ns | 1.0× |
| Reject an invalid color | 48 ns | colord, 92 ns | 1.9× |
| sRGB to OKLCH | 145 ns | culori, 153 ns | 1.1× |
| Mix two colors in OKLab | 283 ns | culori, 669 ns | 2.4× |
| Read one channel | 8 ns | colord, 22 ns | 2.8× |
| Serialize to hex | 44 ns | culori, 66 ns | 1.5× |
| 64-stop OKLab ramp | 2.61 µs | culori, 9.88 µs | 3.8× |
| 128×128 bilinear plane to RGBA | 4.4 ms | culori, 35.3 ms | 8.0× |
Interpolation is where the lead is widest. The 64-stop OKLab ramp is 102× faster than chroma-js, and the bilinear plane follows, since its cost is mixing rather than conversion.
It does not win everything, and the docs publish where it loses. culori converts out of OKLCH faster (101 ns vs 165 ns) and gamut-maps faster (116 ns vs 209 ns), because UrColor routes every conversion through an XYZ D65 hub: 15 bridges instead of 210 direct paths. That same overhead, compounded across 16 384 pixels, is why culori edges the per-pixel channel samplers, and why the components render through WebGL, where one draw call covers the surface at any size.