Appearance
@carrot/colors ​
ts
Zero-dependency color manipulation library with immutable/mutable types across five color spaces, perceptual operations via Oklab/Oklch, and full CSS parsing/rendering.
Installation ​
bash
# From the monorepo root
pnpm installPackage: @carrot/colors (workspace dependency - no npm publish required).
Architecture / How It Works ​
Color Spaces ​
Five color spaces, each with an immutable class, a mutable class, and a structural *Like interface:
- RGB (
ColorRgb) - Canonical type. All conversions route through RGB. - HSL (
ColorHsl) - Hue/saturation/lightness. Good for CSS interop. - HSV (
ColorHsv) - Hue/saturation/value. Good for color pickers. - Oklab (
ColorOklab) - Perceptually uniform cartesian space. - Oklch (
ColorOklch) - Perceptually uniform polar space. Used internally for lighten/darken/saturate/desaturate/hue rotation.
Immutable vs Mutable ​
Immutable classes have readonly fields and with*() methods that return new instances. Mutable classes have writable fields, set(), and copyFrom() for in-place mutation. Convert between them with toMutable() / toImmutable().
Conversion Graph ​
All cross-space conversions go through RGB as the hub:
HSL <-> RGB <-> HSV
|
Oklab <-> OklchThe convert.ts module wraps the raw numeric functions from conversions.ts into typed functions that accept *Like interfaces and return immutable instances.
Parsing Pipeline ​
parseColor() tries each parser in order: hex -> rgb() -> hsl() -> hsv() -> oklab() -> oklch() -> comma-separated bytes. All parsers return ColorRgb | undefined.
Rendering ​
All render functions accept ColorRgbLike and convert internally. renderColor() dispatches by ColorRenderFormat.
Transformations ​
Perceptual operations (lighten, darken, saturate, desaturate, rotateHue) convert to Oklch, apply the transform, and convert back to RGB. This avoids the brightness/saturation shifts you get with naive HSL manipulation.
String-to-Color Utility ​
colorFromString() uses FNV-1a hashing to generate a deterministic hue from any string, then creates a background color and picks black or white text for WCAG contrast. Useful for avatars, tags, and badges.
Dependencies ​
Runtime: None. Dev: typescript ^5.9.3
No dependencies on other @carrot/* packages.
Build ​
bash
cd src/ts/carrot-colors
pnpm build # runs tscOutput: dist/ (ESM, with .d.ts declarations).
"type": "module"in package.json- Node
>=20 <23
Usage Guide ​
Zero-dependency color library with five color spaces, perceptual transforms, CSS parsing/rendering, and blend modes.
Import ​
ts
import {
ColorRgb, ColorHsl, ColorOklch,
parseColor, renderHex, renderOklch,
lighten, darken, lerpPerceptual,
blend, ColorBlendMode,
colorFromString,
} from '@carrot/colors';Common Patterns ​
Parse a CSS color string ​
ts
const color = parseColor('#ff6b35'); // ColorRgb | undefined
const color2 = parseColor('rgb(255, 107, 53)');
const color3 = parseColor('hsl(20, 100%, 60%)');
const color4 = parseColor('oklch(70% 0.18 45)');
const color5 = parseColor('255,107,53'); // comma-separated bytes
// Strict version throws on invalid input
const sure = parseColorStrict('#ff6b35'); // ColorRgb (or throws)Create colors directly ​
ts
const red = new ColorRgb(1, 0, 0); // r,g,b 0-1, alpha defaults to 1
const semiRed = new ColorRgb(1, 0, 0, 0.5); // with alpha
const hsl = new ColorHsl(210, 0.8, 0.5); // h: 0-360, s/l: 0-1
const oklch = new ColorOklch(0.7, 0.15, 180); // L: 0-1, C: 0-0.4, h: 0-360
// Use presets
const white = ColorRgb.white;
const black = ColorRgb.black;
const transparent = ColorRgb.transparent;Convert between color spaces ​
ts
import { rgbToHsl, rgbToOklch, hslToRgb, oklchToRgb } from '@carrot/colors';
const hsl = rgbToHsl(color); // ColorHsl
const oklch = rgbToOklch(color); // ColorOklch
const backToRgb = hslToRgb(hsl); // ColorRgb
const alsoRgb = oklchToRgb(oklch); // ColorRgbRender to CSS strings ​
ts
import { renderHex, renderRgba, renderOklch, renderColor, ColorRenderFormat } from '@carrot/colors';
renderHex(color); // '#ff6b35'
renderRgba(color); // 'rgba(255, 107, 53, 1)'
renderOklch(color); // 'oklch(70% 0.18 45)'
// Dynamic format
renderColor(color, ColorRenderFormat.Hsl); // 'hsl(20, 100%, 60%)'
renderColor(color, ColorRenderFormat.HexRgba); // '#ff6b35ff'Lighten, darken, saturate ​
ts
import { lighten, darken, saturate, desaturate, rotateHue } from '@carrot/colors';
const lighter = lighten(color, 0.1); // +10% Oklch lightness
const darker = darken(color, 0.15); // -15% Oklch lightness
const vivid = saturate(color, 0.05); // +0.05 Oklch chroma
const muted = desaturate(color, 0.05); // -0.05 Oklch chroma
const shifted = rotateHue(color, 120); // rotate hue 120 degreesInterpolate between colors ​
ts
import { lerpRgb, lerpOklch, lerpPerceptual } from '@carrot/colors';
// RGB lerp (fast, but can produce muddy midpoints)
const mid = lerpRgb(colorA, colorB, 0.5);
// Oklch lerp (perceptually uniform, shortest hue path)
const oklchA = rgbToOklch(colorA);
const oklchB = rgbToOklch(colorB);
const midPerceptual = lerpOklch(oklchA, oklchB, 0.5);
// Convenience: RGB in, perceptual lerp, RGB out
const smooth = lerpPerceptual(colorA, colorB, 0.5);
// Generate a gradient
const steps = Array.from({ length: 10 }, (_, i) => lerpPerceptual(colorA, colorB, i / 9));Blend two colors ​
ts
import { blend, ColorBlendMode } from '@carrot/colors';
const result = blend(base, layer, ColorBlendMode.Multiply);
const screen = blend(base, layer, ColorBlendMode.Screen);
const overlay = blend(base, layer, ColorBlendMode.Overlay);
// Also: Normal, Darken, Lighten, ColorDodge, ColorBurn, HardLight, SoftLightAccessibility: luminance and contrast ​
ts
import { luminance, contrastRatio, contrastTextFor } from '@carrot/colors';
const lum = luminance(color); // 0-1 relative luminance (WCAG 2.0)
const ratio = contrastRatio(foreground, bg); // 1 to 21
const textColor = contrastTextFor(bg); // '#000' or '#fff'Deterministic color from a string ​
ts
import { colorFromString } from '@carrot/colors';
const { background, backgroundHex, text } = colorFromString('Jane Smith');
// background: ColorRgb (deterministic, always the same for 'Jane Smith')
// backgroundHex: '#a34fc2' (example)
// text: '#fff' (best contrast)
// Custom saturation/value
const muted = colorFromString('Tag Label', { s: 0.4, v: 0.85 });Immutable with* pattern ​
ts
const base = new ColorRgb(1, 0, 0);
const withAlpha = base.withA(0.5); // new ColorRgb(1, 0, 0, 0.5)
const clamped = base.clamped(); // clamp all channels to 0-1
const eq = base.equals(other); // epsilon comparison (default 1e-6)Mutable colors for performance ​
ts
import { MutableColorRgb } from '@carrot/colors';
const buf = new MutableColorRgb(0, 0, 0);
buf.set(1, 0.5, 0); // mutate in place, returns `this`
buf.copyFrom(otherColor); // copy channels from any ColorRgbLike
buf.clamp(); // clamp in place
const snapshot = buf.toImmutable(); // freeze to ColorRgb