Skip to content

@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 install

Package: @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 <-> Oklch

The 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 tsc

Output: 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);     // ColorRgb

Render 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 degrees

Interpolate 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, SoftLight

Accessibility: 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

Carrot