Swatch & Color Utilities
@domphy/palette exports the Swatch class and a suite of standalone color-space functions. These are the building blocks used internally by Ramp, and you can use them independently to inspect a single color, convert between color spaces, or build custom palette tooling.
Swatch
Swatch wraps a single hex color and lazily computes its CIELAB, LCH, perceptual lightness, and relative luminance.
import { Swatch } from "@domphy/palette"
const s = new Swatch("#3b82f6")swatch.hex
The original hex string exactly as supplied.
s.hex // "#3b82f6"swatch.rgb
Linear sRGB as [r, g, b], each channel in [0, 1]. The input hex is gamma-decoded from sRGB to linear before being stored.
s.rgb // [0.0438, 0.2233, 0.9216]Note: these are linear values, not the raw 8-bit 0–255 values divided by 255. A channel value of 0.9216 does not mean 92% brightness — it means the linear-light contribution of that channel.
swatch.lab
CIELAB coordinates [L, a, b] with D65 reference white. L is perceptual lightness (0–100); a/b are the opponent-color axes (negative a = green, positive a = red, negative b = blue, positive b = yellow).
s.lab // [55.63, 17.5, -64.5]
// L a bswatch.lch
LCH coordinates [L, C, h] — a polar form of CIELAB. C is chroma (roughly "colorfulness", 0–133+), h is hue angle in degrees (0–360).
s.lch // [55.63, 66.77, 285.23]
// L C hue°swatch.lightness
Equivalent Achromatic Lightness (L_EAL) using the High et al. (2023) model. Standard CIE L lightness under-reports how bright a highly-chromatic color looks to the eye — the Helmholtz–Kohlrausch effect. L_EAL corrects for this by adding a hue- and chroma-dependent term.
s.lightness // 73.23 (vs. CIE L = 55.63 — the bright blue looks much lighter)For achromatic colors (white, grey, black) L_EAL equals CIE L exactly because chroma is zero. Differences appear for saturated colors, especially yellow and blue.
This is the value Ramp.lightnessLinearity measures evenness against.
swatch.chroma
LCH chroma — a convenience accessor for lch[1]. Zero for achromatic swatches.
s.chroma // 66.77
new Swatch("#ffffff").chroma // ≈ 0
new Swatch("#000000").chroma // ≈ 0swatch.hue
LCH hue angle in degrees — a convenience accessor for lch[2].
s.hue // 285.23 (blue-violet range)swatch.luminance
Relative luminance following the WCAG 2.x / Rec.709 formula: 0.2126R + 0.7152G + 0.0722B applied to the linear RGB channels. Range [0, 1].
s.luminance // 0.2355Use this to compute WCAG contrast ratios directly:
function wcagContrast(hex1: string, hex2: string): number {
const l1 = new Swatch(hex1).luminance
const l2 = new Swatch(hex2).luminance
const lighter = Math.max(l1, l2)
const darker = Math.min(l1, l2)
return (lighter + 0.05) / (darker + 0.05)
}
wcagContrast("#3b82f6", "#ffffff") // ≈ 3.68 (AA fails for normal text)
wcagContrast("#2354b2", "#ffffff") // ≈ 7.05 (AAA)Color-space conversion functions
All of the following are named exports from @domphy/palette:
import {
hexToRgb, rgbToHex,
rgbToLab, labToRgb,
rgbToOklab, oklabToRgb,
labToLch, lchToLab,
toLightnessEAL, fromLightnessEAL,
calcDeltaE2000,
cssRgbToRgb,
createMonotone,
calcScore, calcStatistics, rootMeanSquare,
} from "@domphy/palette"Hex ↔ linear RGB
const rgb = hexToRgb("#3b82f6") // [0.0438, 0.2233, 0.9216] — linear sRGB
const hex = rgbToHex(rgb) // "#3b82f6"Input hex must be #rrggbb (lowercase or uppercase, both work). Both functions use the standard sRGB gamma curve (γ = 2.4 with a linear toe).
Linear RGB ↔ CIELAB
const lab = rgbToLab([0.0438, 0.2233, 0.9216]) // [55.63, 17.5, -64.5]
const rgb2 = labToRgb([55.63, 17.5, -64.5]) // [0.0438, 0.2233, 0.9216]D65 reference white. The round-trip is accurate within floating-point precision. Values outside the sRGB gamut are not clamped by these functions — clamping happens inside rgbToHex.
Linear RGB ↔ Oklab
Oklab (Björn Ottosson, 2020) is a perceptually uniform color space well-suited for interpolation and gamut mapping. Input and output are linear RGB.
const oklab = rgbToOklab([0.0438, 0.2233, 0.9216])
// [0.546, -0.028, -0.232] — [L, a, b] in Oklab
const rgb3 = oklabToRgb(oklab)
// [0.0438, 0.2233, 0.9216]Oklab L is in [0, 1] (unlike CIELAB L in [0, 100]).
CIELAB ↔ LCH
const lch = labToLch([55.63, 17.5, -64.5]) // [55.63, 66.77, 285.23]
const lab2 = lchToLab([55.63, 66.77, 285.23]) // [55.63, 17.5, -64.5]L_EAL: perceptual lightness
toLightnessEAL takes a CIELAB coordinate and returns Equivalent Achromatic Lightness:
const lab = rgbToLab(hexToRgb("#3b82f6"))
const eal = toLightnessEAL(lab) // 73.23fromLightnessEAL reverses the operation. Given a target L_EAL and a CIELAB coordinate (encoding the chroma and hue of a color), it returns the CIE L that produces that L_EAL:
// "At what CIE L does this blue hue produce L_EAL = 70?"
const cieL = fromLightnessEAL(70, lab) // ≈ 52This is useful when building a ramp generator: you specify evenly-spaced L_EAL targets, then use fromLightnessEAL to find the correct CIE L for each step before interpolating in LCH space.
ΔE2000 color difference
calcDeltaE2000 computes the CIEDE2000 distance between two CIELAB coordinates. A value of 1 is approximately the smallest perceivable difference under optimal viewing conditions; values below 2 are often indistinguishable in practice.
const lab1 = rgbToLab(hexToRgb("#3b82f6")) // blue-500
const lab2 = rgbToLab(hexToRgb("#2f6bd4")) // blue-600
calcDeltaE2000(lab1, lab2) // e.g. 8.4 (one step in a blue ramp)Ramp.deltaECurve calls this for every adjacent pair to build the cumulative distance curve.
CSS rgb() input
cssRgbToRgb parses a CSS rgb() string and returns linear RGB:
cssRgbToRgb("rgb(59, 130, 246)") // [0.0438, 0.2233, 0.9216]Useful when consuming colors from getComputedStyle, design tokens stored as CSS rgb values, or Figma's variable export format.
Statistics helpers
import { calcStatistics, rootMeanSquare, calcScore } from "@domphy/palette"
calcStatistics([0.90, 0.85, 0.88, 0.92])
// { min: 0.85, max: 0.92, avg: 0.8875 }
rootMeanSquare([0.90, 0.85, 0.88, 0.92])
// ≈ 0.8876 — RMS, same formula Palette uses for aggregate metrics
calcScore([0.923, 0.951, 0.887, 0.942, 0.831])
// ≈ 90.7 — geometric mean scaled to 0–100, same as Ramp.scorecalcScore accepts any array of [0, 1] values and returns the geometric mean as a 0–100 number. You can use it to compose custom quality signals alongside the built-in ones:
import { calcScore } from "@domphy/palette"
// Custom metric: fraction of steps that are not too dark for body text
const usableSteps = ramp.swatches.filter(s => s.lightness > 30 && s.lightness < 85).length
const usabilityScore = usableSteps / ramp.steps
// Blend into your own composite score
const combined = calcScore([
ramp.metrics.lightnessLinearity,
ramp.metrics.chromaSmoothness,
usabilityScore,
])Monotone cubic spline
createMonotone builds a monotone cubic Hermite interpolator from [x, y] control points. It guarantees no overshoot between adjacent points (Fritsch–Carlson 1980), which is why Ramp.chromaSmoothness uses it to fit a reference chroma arc.
import { createMonotone } from "@domphy/palette"
// Control points: (step index, chroma value)
const interp = createMonotone([[0, 10], [6, 133], [17, 5]])
interp(3) // interpolated chroma at step 3 — on the ascending arc
interp(12) // interpolated chroma at step 12 — on the descending arcThis is primarily an internal utility, but it is exported for ramp generators that need to fit a smooth chroma target before generating colors.