[{"data":1,"prerenderedAt":563},["ShallowReactive",2],{"project-urcolor-en":3,"mdc--41iiuj-key":34},{"id":4,"title":5,"content":6,"description":7,"endDate":8,"extension":9,"image":8,"meta":10,"poster":30,"startDate":29,"stem":31,"style":19,"tags":32,"url":18,"__hash__":33},"projects_en/en/projects/urcolor.yml","UrColor — Universal Color Picker Library","> Part of **UrStack**, my open-source organization for TypeScript libraries.\nSiblings: [UrVis](/projects/urvis), [UrCharts](/projects/urcharts), [UrDock](/projects/urdock).\n\n## Overview\n\nUrColor is a headless component library for building color interfaces. It ships\nthe behaviour, meaning pointer math, keyboard handling, color conversion and ARIA\nwiring, and leaves every pixel of the presentation to the consumer.\n\nEight component families cover the vocabulary of color UI: area, slider, field,\nswatch, swatch picker, wheel, triangle, ring. Each one ships in all four bindings\n(Vue, React, Svelte, Angular) and works across 15 color spaces, from sRGB and HSL\nthrough OKLCH, Lab, Display P3 and CIE XYZ, on a zero-dependency CSS Color 4\nengine.\n\n---\n\n## Why I built it\n\nEvery color picker I reached for made the same trade. Either it looked exactly\nthe way its authors wanted and fought every attempt to restyle it, or it was\nperceptually naive, interpolating gradients in sRGB and producing muddy ramps in\nmodern color spaces.\n\nThe gap is structural, not accidental. React Aria and Reka UI both declare the\nidentical public type, `ColorSpace = 'rgb' | 'hsl' | 'hsb'`, and Ark UI and\nZag.js share those same three. Every other picker on npm stores HSV or RGB and\nconverts on the way out. An OKLCH area, a Lab slider or a P3 gamut boundary\ncannot be expressed by any of them, which is also why none of them need a GPU\nrenderer.\n\nUrColor separates the two concerns. The library owns correctness and\naccessibility, the design system owns appearance.\n\n---\n\n## Key features\n\n- Eight component families: area, slider, field, swatch, swatch picker, wheel,\n  triangle, ring\n- 15 color spaces: sRGB, linear sRGB, HSL, HSV, HWB, Lab, LCh, OKLab, OKLCH,\n  Display P3, A98 RGB, ProPhoto RGB, Rec. 2020, XYZ D65, XYZ D50\n- Vue 3, React 18, Svelte 5 and Angular 21 bindings over a shared core, so the\n  behaviour is the same in all four; Solid is planned\n- WAI-ARIA compliance, with full keyboard control and screen reader support,\n  modelled on Adobe's React Spectrum\n- WebGL shaders that draw perceptually accurate Lab and OKLCH surfaces CSS\n  gradients cannot express\n- CPU grid samplers (`sampleBilinearGrid`, `samplePolarGrid` and friends) that\n  return `Uint8ClampedArray` RGBA ready for `putImageData`, which no other color\n  library ships\n- Color science: ΔE CIEDE2000 and ΔE OK, WCAG 2.1 and APCA contrast, CSS Color 4\n  gamut mapping, hue-arc control\n- Zero runtime dependencies, since the CSS Color 4 engine is written from scratch\n- Optional i18n, in a separate package that names colors in up to 298 languages\n  and translates channel labels into 77\n\n---\n\n## Benchmarks\n\n`@urcolor/core` is benchmarked against culori, colorjs.io, chroma-js, colord,\ntinycolor2 and @ctrl/tinycolor on every operation a picker performs: 59 groups,\nApple M1, Bun 1.3, mitata. It leads most of them.\n\n| Operation | UrColor | Next fastest | Margin |\n| --- | --- | --- | --- |\n| Parse a hex color | **137 ns** | colord, 138 ns | 1.0× |\n| Reject an invalid color | **48 ns** | colord, 92 ns | 1.9× |\n| sRGB to OKLCH | **145 ns** | culori, 153 ns | 1.1× |\n| Mix two colors in OKLab | **283 ns** | culori, 669 ns | 2.4× |\n| Read one channel | **8 ns** | colord, 22 ns | 2.8× |\n| Serialize to hex | **44 ns** | culori, 66 ns | 1.5× |\n| 64-stop OKLab ramp | **2.61 µs** | culori, 9.88 µs | 3.8× |\n| 128×128 bilinear plane to RGBA | **4.4 ms** | culori, 35.3 ms | 8.0× |\n\nInterpolation is where the lead is widest. The 64-stop OKLab ramp is 102× faster\nthan chroma-js, and the bilinear plane follows, since its cost is mixing rather\nthan conversion.\n\nIt does not win everything, and the docs publish where it loses. culori converts\n*out* of OKLCH faster (101 ns vs 165 ns) and gamut-maps faster (116 ns vs\n209 ns), because UrColor routes every conversion through an XYZ D65 hub: 15\nbridges instead of 210 direct paths. That same overhead, compounded across\n16 384 pixels, is why culori edges the per-pixel channel samplers, and why the\ncomponents render through WebGL, where one draw call covers the surface at any\nsize.\n\n---\n\n## Tech stack\n\n::tech-carousel{:tech='[\"typescript\", \"vue\", \"react\", \"svelte\", \"angular\", \"webgl\", \"vitepress\"]'}\n::\n\n- **Core**: TypeScript, custom CSS Color 4 engine, WebGL\n- **Bindings**: Vue (on Reka UI), React, Svelte, Angular\n- **Benchmarks**: mitata on Bun\n- **Docs**: VitePress\n","Headless, accessible color picker primitives across 15 color spaces and four frameworks, with a benchmarked zero-dependency color engine and GPU-rendered gradients.",null,"yml",{"path":11,"body":12},"/en/projects/urcolor",{"title":5,"description":7,"poster":13,"url":18,"style":19,"tags":20,"startDate":29,"content":6},{"pattern":14,"gradient":15,"color":16,"bg":17},"circle","conic","#ff4081","#1a1a2e","https://urcolor.vercel.app/","projects/urcolor-styles.css",[21,22,23,24,25,26,27,28],"UrStack","TypeScript","Vue","React","Svelte","Angular","WebGL","Accessibility","2026",{"pattern":14,"gradient":15,"color":16,"bg":17},"en/projects/urcolor",[21,22,23,24,25,26,27,28],"gZfukl6DcT-F1jGVugy5HHhTQ7RS903SpTHVgz8Ol-A",{"data":35,"body":36},{},{"type":37,"children":38},"root",[39,82,89,94,99,103,109,114,128,133,136,142,223,226,232,243,489,494,507,510,516,521],{"type":40,"tag":41,"props":42,"children":43},"element","blockquote",{},[44],{"type":40,"tag":45,"props":46,"children":47},"p",{},[48,51,56,58,65,67,73,74,80],{"type":49,"value":50},"text","Part of ",{"type":40,"tag":52,"props":53,"children":54},"strong",{},[55],{"type":49,"value":21},{"type":49,"value":57},", my open-source organization for TypeScript libraries.\nSiblings: ",{"type":40,"tag":59,"props":60,"children":62},"a",{"href":61},"/projects/urvis",[63],{"type":49,"value":64},"UrVis",{"type":49,"value":66},", ",{"type":40,"tag":59,"props":68,"children":70},{"href":69},"/projects/urcharts",[71],{"type":49,"value":72},"UrCharts",{"type":49,"value":66},{"type":40,"tag":59,"props":75,"children":77},{"href":76},"/projects/urdock",[78],{"type":49,"value":79},"UrDock",{"type":49,"value":81},".",{"type":40,"tag":83,"props":84,"children":86},"h2",{"id":85},"overview",[87],{"type":49,"value":88},"Overview",{"type":40,"tag":45,"props":90,"children":91},{},[92],{"type":49,"value":93},"UrColor is a headless component library for building color interfaces. It ships\nthe behaviour, meaning pointer math, keyboard handling, color conversion and ARIA\nwiring, and leaves every pixel of the presentation to the consumer.",{"type":40,"tag":45,"props":95,"children":96},{},[97],{"type":49,"value":98},"Eight component families cover the vocabulary of color UI: area, slider, field,\nswatch, swatch picker, wheel, triangle, ring. Each one ships in all four bindings\n(Vue, React, Svelte, Angular) and works across 15 color spaces, from sRGB and HSL\nthrough OKLCH, Lab, Display P3 and CIE XYZ, on a zero-dependency CSS Color 4\nengine.",{"type":40,"tag":100,"props":101,"children":102},"hr",{},[],{"type":40,"tag":83,"props":104,"children":106},{"id":105},"why-i-built-it",[107],{"type":49,"value":108},"Why I built it",{"type":40,"tag":45,"props":110,"children":111},{},[112],{"type":49,"value":113},"Every color picker I reached for made the same trade. Either it looked exactly\nthe way its authors wanted and fought every attempt to restyle it, or it was\nperceptually naive, interpolating gradients in sRGB and producing muddy ramps in\nmodern color spaces.",{"type":40,"tag":45,"props":115,"children":116},{},[117,119,126],{"type":49,"value":118},"The gap is structural, not accidental. React Aria and Reka UI both declare the\nidentical public type, ",{"type":40,"tag":120,"props":121,"children":123},"code",{"className":122},[],[124],{"type":49,"value":125},"ColorSpace = 'rgb' | 'hsl' | 'hsb'",{"type":49,"value":127},", and Ark UI and\nZag.js share those same three. Every other picker on npm stores HSV or RGB and\nconverts on the way out. An OKLCH area, a Lab slider or a P3 gamut boundary\ncannot be expressed by any of them, which is also why none of them need a GPU\nrenderer.",{"type":40,"tag":45,"props":129,"children":130},{},[131],{"type":49,"value":132},"UrColor separates the two concerns. The library owns correctness and\naccessibility, the design system owns appearance.",{"type":40,"tag":100,"props":134,"children":135},{},[],{"type":40,"tag":83,"props":137,"children":139},{"id":138},"key-features",[140],{"type":49,"value":141},"Key features",{"type":40,"tag":143,"props":144,"children":145},"ul",{},[146,152,157,162,167,172,208,213,218],{"type":40,"tag":147,"props":148,"children":149},"li",{},[150],{"type":49,"value":151},"Eight component families: area, slider, field, swatch, swatch picker, wheel,\ntriangle, ring",{"type":40,"tag":147,"props":153,"children":154},{},[155],{"type":49,"value":156},"15 color spaces: sRGB, linear sRGB, HSL, HSV, HWB, Lab, LCh, OKLab, OKLCH,\nDisplay P3, A98 RGB, ProPhoto RGB, Rec. 2020, XYZ D65, XYZ D50",{"type":40,"tag":147,"props":158,"children":159},{},[160],{"type":49,"value":161},"Vue 3, React 18, Svelte 5 and Angular 21 bindings over a shared core, so the\nbehaviour is the same in all four; Solid is planned",{"type":40,"tag":147,"props":163,"children":164},{},[165],{"type":49,"value":166},"WAI-ARIA compliance, with full keyboard control and screen reader support,\nmodelled on Adobe's React Spectrum",{"type":40,"tag":147,"props":168,"children":169},{},[170],{"type":49,"value":171},"WebGL shaders that draw perceptually accurate Lab and OKLCH surfaces CSS\ngradients cannot express",{"type":40,"tag":147,"props":173,"children":174},{},[175,177,183,184,190,192,198,200,206],{"type":49,"value":176},"CPU grid samplers (",{"type":40,"tag":120,"props":178,"children":180},{"className":179},[],[181],{"type":49,"value":182},"sampleBilinearGrid",{"type":49,"value":66},{"type":40,"tag":120,"props":185,"children":187},{"className":186},[],[188],{"type":49,"value":189},"samplePolarGrid",{"type":49,"value":191}," and friends) that\nreturn ",{"type":40,"tag":120,"props":193,"children":195},{"className":194},[],[196],{"type":49,"value":197},"Uint8ClampedArray",{"type":49,"value":199}," RGBA ready for ",{"type":40,"tag":120,"props":201,"children":203},{"className":202},[],[204],{"type":49,"value":205},"putImageData",{"type":49,"value":207},", which no other color\nlibrary ships",{"type":40,"tag":147,"props":209,"children":210},{},[211],{"type":49,"value":212},"Color science: ΔE CIEDE2000 and ΔE OK, WCAG 2.1 and APCA contrast, CSS Color 4\ngamut mapping, hue-arc control",{"type":40,"tag":147,"props":214,"children":215},{},[216],{"type":49,"value":217},"Zero runtime dependencies, since the CSS Color 4 engine is written from scratch",{"type":40,"tag":147,"props":219,"children":220},{},[221],{"type":49,"value":222},"Optional i18n, in a separate package that names colors in up to 298 languages\nand translates channel labels into 77",{"type":40,"tag":100,"props":224,"children":225},{},[],{"type":40,"tag":83,"props":227,"children":229},{"id":228},"benchmarks",[230],{"type":49,"value":231},"Benchmarks",{"type":40,"tag":45,"props":233,"children":234},{},[235,241],{"type":40,"tag":120,"props":236,"children":238},{"className":237},[],[239],{"type":49,"value":240},"@urcolor/core",{"type":49,"value":242}," is benchmarked against culori, colorjs.io, chroma-js, colord,\ntinycolor2 and @ctrl/tinycolor on every operation a picker performs: 59 groups,\nApple M1, Bun 1.3, mitata. It leads most of them.",{"type":40,"tag":244,"props":245,"children":246},"table",{},[247,276],{"type":40,"tag":248,"props":249,"children":250},"thead",{},[251],{"type":40,"tag":252,"props":253,"children":254},"tr",{},[255,261,266,271],{"type":40,"tag":256,"props":257,"children":258},"th",{},[259],{"type":49,"value":260},"Operation",{"type":40,"tag":256,"props":262,"children":263},{},[264],{"type":49,"value":265},"UrColor",{"type":40,"tag":256,"props":267,"children":268},{},[269],{"type":49,"value":270},"Next fastest",{"type":40,"tag":256,"props":272,"children":273},{},[274],{"type":49,"value":275},"Margin",{"type":40,"tag":277,"props":278,"children":279},"tbody",{},[280,307,333,359,385,411,437,463],{"type":40,"tag":252,"props":281,"children":282},{},[283,289,297,302],{"type":40,"tag":284,"props":285,"children":286},"td",{},[287],{"type":49,"value":288},"Parse a hex color",{"type":40,"tag":284,"props":290,"children":291},{},[292],{"type":40,"tag":52,"props":293,"children":294},{},[295],{"type":49,"value":296},"137 ns",{"type":40,"tag":284,"props":298,"children":299},{},[300],{"type":49,"value":301},"colord, 138 ns",{"type":40,"tag":284,"props":303,"children":304},{},[305],{"type":49,"value":306},"1.0×",{"type":40,"tag":252,"props":308,"children":309},{},[310,315,323,328],{"type":40,"tag":284,"props":311,"children":312},{},[313],{"type":49,"value":314},"Reject an invalid color",{"type":40,"tag":284,"props":316,"children":317},{},[318],{"type":40,"tag":52,"props":319,"children":320},{},[321],{"type":49,"value":322},"48 ns",{"type":40,"tag":284,"props":324,"children":325},{},[326],{"type":49,"value":327},"colord, 92 ns",{"type":40,"tag":284,"props":329,"children":330},{},[331],{"type":49,"value":332},"1.9×",{"type":40,"tag":252,"props":334,"children":335},{},[336,341,349,354],{"type":40,"tag":284,"props":337,"children":338},{},[339],{"type":49,"value":340},"sRGB to OKLCH",{"type":40,"tag":284,"props":342,"children":343},{},[344],{"type":40,"tag":52,"props":345,"children":346},{},[347],{"type":49,"value":348},"145 ns",{"type":40,"tag":284,"props":350,"children":351},{},[352],{"type":49,"value":353},"culori, 153 ns",{"type":40,"tag":284,"props":355,"children":356},{},[357],{"type":49,"value":358},"1.1×",{"type":40,"tag":252,"props":360,"children":361},{},[362,367,375,380],{"type":40,"tag":284,"props":363,"children":364},{},[365],{"type":49,"value":366},"Mix two colors in OKLab",{"type":40,"tag":284,"props":368,"children":369},{},[370],{"type":40,"tag":52,"props":371,"children":372},{},[373],{"type":49,"value":374},"283 ns",{"type":40,"tag":284,"props":376,"children":377},{},[378],{"type":49,"value":379},"culori, 669 ns",{"type":40,"tag":284,"props":381,"children":382},{},[383],{"type":49,"value":384},"2.4×",{"type":40,"tag":252,"props":386,"children":387},{},[388,393,401,406],{"type":40,"tag":284,"props":389,"children":390},{},[391],{"type":49,"value":392},"Read one channel",{"type":40,"tag":284,"props":394,"children":395},{},[396],{"type":40,"tag":52,"props":397,"children":398},{},[399],{"type":49,"value":400},"8 ns",{"type":40,"tag":284,"props":402,"children":403},{},[404],{"type":49,"value":405},"colord, 22 ns",{"type":40,"tag":284,"props":407,"children":408},{},[409],{"type":49,"value":410},"2.8×",{"type":40,"tag":252,"props":412,"children":413},{},[414,419,427,432],{"type":40,"tag":284,"props":415,"children":416},{},[417],{"type":49,"value":418},"Serialize to hex",{"type":40,"tag":284,"props":420,"children":421},{},[422],{"type":40,"tag":52,"props":423,"children":424},{},[425],{"type":49,"value":426},"44 ns",{"type":40,"tag":284,"props":428,"children":429},{},[430],{"type":49,"value":431},"culori, 66 ns",{"type":40,"tag":284,"props":433,"children":434},{},[435],{"type":49,"value":436},"1.5×",{"type":40,"tag":252,"props":438,"children":439},{},[440,445,453,458],{"type":40,"tag":284,"props":441,"children":442},{},[443],{"type":49,"value":444},"64-stop OKLab ramp",{"type":40,"tag":284,"props":446,"children":447},{},[448],{"type":40,"tag":52,"props":449,"children":450},{},[451],{"type":49,"value":452},"2.61 µs",{"type":40,"tag":284,"props":454,"children":455},{},[456],{"type":49,"value":457},"culori, 9.88 µs",{"type":40,"tag":284,"props":459,"children":460},{},[461],{"type":49,"value":462},"3.8×",{"type":40,"tag":252,"props":464,"children":465},{},[466,471,479,484],{"type":40,"tag":284,"props":467,"children":468},{},[469],{"type":49,"value":470},"128×128 bilinear plane to RGBA",{"type":40,"tag":284,"props":472,"children":473},{},[474],{"type":40,"tag":52,"props":475,"children":476},{},[477],{"type":49,"value":478},"4.4 ms",{"type":40,"tag":284,"props":480,"children":481},{},[482],{"type":49,"value":483},"culori, 35.3 ms",{"type":40,"tag":284,"props":485,"children":486},{},[487],{"type":49,"value":488},"8.0×",{"type":40,"tag":45,"props":490,"children":491},{},[492],{"type":49,"value":493},"Interpolation is where the lead is widest. The 64-stop OKLab ramp is 102× faster\nthan chroma-js, and the bilinear plane follows, since its cost is mixing rather\nthan conversion.",{"type":40,"tag":45,"props":495,"children":496},{},[497,499,505],{"type":49,"value":498},"It does not win everything, and the docs publish where it loses. culori converts\n",{"type":40,"tag":500,"props":501,"children":502},"em",{},[503],{"type":49,"value":504},"out",{"type":49,"value":506}," of OKLCH faster (101 ns vs 165 ns) and gamut-maps faster (116 ns vs\n209 ns), because UrColor routes every conversion through an XYZ D65 hub: 15\nbridges instead of 210 direct paths. That same overhead, compounded across\n16 384 pixels, is why culori edges the per-pixel channel samplers, and why the\ncomponents render through WebGL, where one draw call covers the surface at any\nsize.",{"type":40,"tag":100,"props":508,"children":509},{},[],{"type":40,"tag":83,"props":511,"children":513},{"id":512},"tech-stack",[514],{"type":49,"value":515},"Tech stack",{"type":40,"tag":517,"props":518,"children":520},"tech-carousel",{":tech":519},"[\"typescript\", \"vue\", \"react\", \"svelte\", \"angular\", \"webgl\", \"vitepress\"]",[],{"type":40,"tag":143,"props":522,"children":523},{},[524,534,544,553],{"type":40,"tag":147,"props":525,"children":526},{},[527,532],{"type":40,"tag":52,"props":528,"children":529},{},[530],{"type":49,"value":531},"Core",{"type":49,"value":533},": TypeScript, custom CSS Color 4 engine, WebGL",{"type":40,"tag":147,"props":535,"children":536},{},[537,542],{"type":40,"tag":52,"props":538,"children":539},{},[540],{"type":49,"value":541},"Bindings",{"type":49,"value":543},": Vue (on Reka UI), React, Svelte, Angular",{"type":40,"tag":147,"props":545,"children":546},{},[547,551],{"type":40,"tag":52,"props":548,"children":549},{},[550],{"type":49,"value":231},{"type":49,"value":552},": mitata on Bun",{"type":40,"tag":147,"props":554,"children":555},{},[556,561],{"type":40,"tag":52,"props":557,"children":558},{},[559],{"type":49,"value":560},"Docs",{"type":49,"value":562},": VitePress",1785819619093]