Documentation

Python and JavaScript with identical APIs. TypeScript types included.

Installation

Helmlab works in both Python and JavaScript. Pick whichever your team already uses. If you just want to try it out, the JS package is the easiest to get going with.

Zero dependencies in the JS package (17.8KB gzipped). Python requires NumPy. Both packages ship the same default parameters and produce identical output.

# Python
pip install helmlab
# npm
npm install helmlab

# or yarn / pnpm
yarn add helmlab

PostCSS plugin also available: npm install postcss-helmlab

Helmlab is also merged into Color.js master (3 spaces, pending next release) — see Using with Color.js below.

Quick Start

The Helmlab class is the main entry point. It gives you everything: palettes, gradients, contrast checking, and color measurement -- all in one place.

What you get: Pass in any hex color and get back a full palette, smooth gradients, or a Tailwind-style 50-950 semantic scale. Every output is gamut-mapped to valid sRGB.

Helmlab is the high-level API with three namespaces: hl.gen (create colors — GenSpace), hl.metric (measure colors — MetricSpace) and hl.tokens (design-token export). It accepts hex/CSS color strings, returns color strings, and handles gamut mapping internally.

from helmlab import Helmlab

hl = Helmlab()

# Generate a 10-shade palette
palette = hl.gen.palette("#3b82f6", steps=10)

# Smooth gradient from red to blue (16 steps)
gradient = hl.gen.gradient("#ef4444", "#3b82f6", steps=16)

# Color distance (trained perceptual metric)
dist = hl.metric.difference("#ff0000", "#00ff00")

# Ensure WCAG AA contrast (adjusts color if needed)
adjusted = hl.gen.ensure_contrast("#3b82f6", "#ffffff", ratio=4.5)

# Tailwind-style semantic scale
scale = hl.gen.scale("#3b82f6")
import { Helmlab } from 'helmlab';

const hl = new Helmlab();

// Generate a 10-shade palette
const palette = hl.gen.palette('#3b82f6', 10);

// Smooth gradient from red to blue (16 steps)
const gradient = hl.gen.gradient('#ef4444', '#3b82f6', 16);

// Color distance (trained perceptual metric)
const dist = hl.metric.difference('#ff0000', '#00ff00');

// Ensure WCAG AA contrast (adjusts color if needed)
const adjusted = hl.gen.ensureContrast('#3b82f6', '#ffffff', 4.5);

// Tailwind-style semantic scale
const scale = hl.gen.scale('#3b82f6');

GenSpace

GenSpace is the color space optimized for creating colors -- gradients, palettes, and gamut mapping. It produces smooth, visually pleasing gradients that keep slightly more saturation through the blue→white midpoint than OKLab (both stay blue; the gap is small but consistent).

When to use: Generating color palettes, creating gradients, building design tokens, gamut mapping for display.

GenSpace uses a depressed cubic transfer function (y³ + αy = x) with L-gated hue enrichment. Pipeline: XYZ → M1 → depcubic → M2 → PW_L → enrichment → C^cp → NC → Lab. All transforms have exact analytical inverses.

Forward / Inverse Transform

from helmlab import Helmlab

hl = Helmlab()

# sRGB [0,1] → GenSpace Lab [L, a, b]
lab = hl.gen.from_srgb([0.231, 0.510, 0.965])

# GenSpace Lab → sRGB [0,1]
rgb = hl.gen.to_srgb(lab)

# XYZ → Lab and back (raw GenSpace via hl.gen.space)
lab = hl.gen.space.from_XYZ(xyz)
xyz = hl.gen.space.to_XYZ(lab)
import { Helmlab } from 'helmlab';

const hl = new Helmlab();

// sRGB [0,1] → GenSpace Lab [L, a, b]
const lab = hl.gen.fromSrgb([0.231, 0.510, 0.965]);

// GenSpace Lab → sRGB [0,1]
const rgb = hl.gen.toSrgb(lab);

// XYZ → Lab and back (raw GenSpace via hl.gen.space)
const lab2 = hl.gen.space.fromXYZ(xyz);
const xyz2 = hl.gen.space.toXYZ(lab2);

Gradient Generation

Helmlab gradients use arc-length reparameterization to ensure each step looks equally spaced to human eyes. This means no bunching near the endpoints and no sudden jumps in the middle.

from helmlab import Helmlab

hl = Helmlab()

# 16-step gradient with arc-length reparameterization
grad = hl.gen.gradient("#ef4444", "#3b82f6", steps=16)
# Returns: ['#ef4444', '#e64f55', '#dc5864', ..., '#3b82f6']
import { Helmlab } from 'helmlab';

const hl = new Helmlab();

// 16-step gradient with arc-length reparameterization
const grad = hl.gen.gradient('#ef4444', '#3b82f6', 16);
// Returns: ['#ef4444', '#e64f55', '#dc5864', ..., '#3b82f6']

Palette Generation

# Generate a 10-shade palette (light to dark)
palette = hl.gen.palette("#3b82f6", steps=10)
# Returns: ['#fafcff', '#ccdeff', ..., '#3e86fa', ..., '#000046']
// Generate a 10-shade palette (light to dark)
const palette = hl.gen.palette('#3b82f6', 10);
// Returns: ['#fafcff', '#ccdeff', ..., '#3e86fa', ..., '#000046']

Key properties: 360/360/360 valid cusps across sRGB, Display P3, and Rec.2020. Zero monotonicity violations in sRGB/P3 (1 in Rec.2020). Blue-to-white gradient midpoint (G/R ≈ 1.5) stays a touch more saturated than OKLab (≈ 1.4) — both blue. Achromatic precision: C* ≈ 10-15.

MetricSpace

MetricSpace is optimized for measuring how different two colors look. It answers the question "how far apart do these colors appear to a human?" more accurately than any existing formula, including the industry-standard CIEDE2000.

When to use: Comparing colors, finding closest matches, evaluating palette consistency, quality control for design tokens.

72-parameter enriched pipeline jointly optimized against COMBVD (3,813 pairs, 64,000+ human judgments). STRESS 22.48 -- 23% better than CIEDE2000's 29.2. Pipeline: XYZ → M1 → γ → M2 → Hue → H-K → L → C → HL → NC → φ → Lab.

Color Difference

The recommended entry point is metric.difference() — it returns the trained perceptual distance directly from hex (the full Minkowski + compression metric optimized against COMBVD, STRESS 22.48, 23% better than CIEDE2000). Three companions are also exposed: metric.euclidean() returns uncompressed Euclidean Lab — ΔE76-style, fast, for quick UI comparisons — metric.ciede2000() is the industry-standard formula (best for catalog matching), and metric.jnd() expresses the difference in just-noticeable-difference units. For Lab inputs, metric.distance() is the same trained metric with an identical contract in both languages.

Experimental: metric.confidence() returns the difference plus how much observers will disagree about it — including p_noticeable, the chance a random observer rates the pair at least moderately different (small / low-chroma differences come back unreliable). v2: ordered-probit refit of our 47-pair × 71-observer set — the categorical ratings are modelled as thresholds on a latent axis, fixing v1's inverted disagreement target. Available in both Python (metric.confidence) and JS (metric.confidence) — identical output.

from helmlab import Helmlab

hl = Helmlab()

# Recommended: perceptual difference from hex (COMBVD-fit metric)
dist = hl.metric.difference("#ff0000", "#00ff00")
# → ~0.149 (saturates near 0.15 for very dissimilar pairs)

# Difference + observer-disagreement reliability (experimental)
r = hl.metric.confidence("#808080", "#828282")
# → de=0.012, p_noticeable=0.08, reliability=0.41, reliable=False

# CIEDE2000 (industry standard, CIELAB scale ~0-100)
de = hl.metric.ciede2000("#ff0000", "#00ff00")
# → 86.61

# Catalog matching: nearest color from a fixed palette
# default metric is ciede2000: measurably the most perturbation-stable
# choice for suprathreshold argmax (7 vs 16 flips/100 at ±2/255 in our tests)
pick = hl.metric.nearest("#fbf0d0", palette)
# → {hex, index, distance, runner_up, margin} — small margin = fragile pick

# Uncompressed Euclidean Lab (fast, for quick UI checks)
dist = hl.metric.euclidean("#ff0000", "#00ff00")
# → 1.62 — black/white → 1.12; range 0–1.6+

# Contrast ratio (WCAG-compatible, lives on the gen namespace)
ratio = hl.gen.contrast_ratio("#ffffff", "#3b82f6")
# → 3.68
import { Helmlab } from 'helmlab';

const hl = new Helmlab();

// Trained perceptual difference (hex inputs, COMBVD-fit)
const dist = hl.metric.difference('#ff0000', '#00ff00');

// Euclidean Lab distance (uncompressed)
const de = hl.metric.euclidean('#ff0000', '#00ff00');
// → 1.62 — black/white → 1.12; range 0–1.6+

// Same trained metric on MetricLab inputs
const lab1 = hl.metric.fromHex('#ff0000');
const lab2 = hl.metric.fromHex('#00ff00');
const distP = hl.metric.distance(lab1, lab2);

// Contrast ratio (WCAG-compatible, lives on the gen namespace)
const ratio = hl.gen.contrastRatio('#ffffff', '#3b82f6');
// → 3.68

STRESS Evaluation

from helmlab.spaces.metric import MetricSpace

metric = MetricSpace()

# Forward transform: XYZ → MetricSpace Lab
lab = metric.from_XYZ(xyz)

# Raw-class distance takes XYZ inputs in Python
dist = metric.distance(xyz1, xyz2)

# For Lab inputs prefer the facade — identical contract in both languages
dist = hl.metric.distance(lab1, lab2)
import { MetricSpace, compileParams, getDefaultParams } from 'helmlab';

const metric = new MetricSpace(compileParams(getDefaultParams()));

// Forward transform: XYZ → MetricSpace Lab
const lab = metric.fromXYZ(xyz);

// Raw-class distance takes Lab inputs in JS; prefer the facade
// hl.metric.distance(lab1, lab2) — identical contract in both languages
const dist = metric.distance(lab1, lab2);

Benchmark: STRESS 22.48 on COMBVD (with Bradford CAT). Beats CIEDE2000 (29.2), CIE94 (33.37), CAM16-UCS (33.47), CIECAM02-UCS (30.9), and every other published Lab-only formula. Includes Helmholtz-Kohlrausch compensation, hue-dependent lightness, chroma scaling, and neutral correction.

Helmlab Class

The Helmlab class is the single import you need for most tasks. It exposes three namespaces — hl.gen to create colors, hl.metric to measure them, hl.tokens to export design tokens — with a clean hex-in, hex-out API.

High-level API class. Accepts hex/CSS color strings, handles sRGB conversion and gamut mapping internally. hl.gen (GenSpace) covers generation and contrast; hl.metric (MetricSpace) covers distance and gamut queries. The two namespaces have branded Lab types (GenLab / MetricLab) — passing one space's Lab into the other's API throws.

gen.palette()

Generate a lightness-uniform palette from a single color.

palette = hl.gen.palette("#3b82f6", steps=10)
# Returns list of hex strings: ['#fafcff', '#ccdeff', ...]
const palette = hl.gen.palette('#3b82f6', 10);
// Returns string[]: ['#fafcff', '#ccdeff', ...]

gen.gradient()

Generate a perceptually uniform gradient between two colors using CIEDE2000 arc-length reparameterization. Pass gamut: 'display-p3' (or 'rec2020') for wide-gamut CSS stops.

grad = hl.gen.gradient("#ef4444", "#3b82f6", steps=16)
# Steps are equally spaced in CIEDE2000 perceptual distance
const grad = hl.gen.gradient('#ef4444', '#3b82f6', 16);
// Steps are equally spaced in CIEDE2000 perceptual distance

gen.scale()

Generate a Tailwind-style 50-950 semantic scale from a base color. Level 500 is the input color exactly.

scale = hl.gen.scale("#3b82f6")
# Returns dict: {50: '#e7efff', 100: '#d4e3ff', ..., 950: '#000046'}
const scale = hl.gen.scale('#3b82f6');
// Returns Record<number, string>: {50: '#e7efff', 100: '#d4e3ff', ...}

gen.adaptToMode()

Adapt a color between light and dark mode via soft L-inversion in GenSpace.

# Light → dark
dark = hl.gen.adapt_to_mode("#3b82f6", from_mode="light", to_mode="dark")

# Dark → light
light = hl.gen.adapt_to_mode("#93c5fd", from_mode="dark", to_mode="light")
// Light → dark
const dark = hl.gen.adaptToMode('#3b82f6', 'light', 'dark');

// Dark → light
const light = hl.gen.adaptToMode('#93c5fd', 'dark', 'light');

gen.ensureContrast()

Adjust a color to meet a minimum WCAG contrast ratio against a background.

# Ensure WCAG AA (4.5:1) contrast
adjusted = hl.gen.ensure_contrast("#3b82f6", "#ffffff", ratio=4.5)

# Ensure WCAG AAA (7:1) contrast — strict=True raises if unreachable
adjusted = hl.gen.ensure_contrast("#3b82f6", "#ffffff", ratio=7.0)
// Ensure WCAG AA (4.5:1) contrast
const adjusted = hl.gen.ensureContrast('#3b82f6', '#ffffff', 4.5);

// Ensure WCAG AAA (7:1) contrast — { strict: true } throws if unreachable
const adjusted2 = hl.gen.ensureContrast('#3b82f6', '#ffffff', 7.0);

gen.meetsContrast()

Check if a foreground/background pair meets a WCAG contrast level without modifying the colors.

hl.gen.meets_contrast("#3b82f6", "#ffffff", level="AA")   # False
hl.gen.meets_contrast("#1e40af", "#ffffff", level="AAA")  # True
hl.gen.meetsContrast('#3b82f6', '#ffffff', 'AA');   // false
hl.gen.meetsContrast('#1e40af', '#ffffff', 'AAA');  // true

gen.adaptPair()

Adapt a foreground/background pair to a target mode while guaranteeing minimum contrast ratio.

# Returns (fg_hex, bg_hex) tuple with contrast ≥ 4.5
fg, bg = hl.gen.adapt_pair(
    "#3366ff", "#ffffff",
    from_mode="light", to_mode="dark", ratio=4.5
)
// Returns [fgHex, bgHex] with contrast ≥ 4.5
const [fg, bg] = hl.gen.adaptPair('#3366ff', '#ffffff', 'light', 'dark', 4.5);

metric.info()

Get detailed color information: hex, sRGB, XYZ, Lab (metric), L/C/H, relative luminance, and gamut membership (sRGB / P3 / Rec.2020).

info = hl.metric.info("#3b82f6")
# {hex: '#3b82f6', srgb: [0.231, 0.510, 0.965],
#  xyz: [0.264, 0.235, 0.903], lab: [0.722, -0.247, -0.238],
#  L: 0.722, C: 0.343, H: 223.9, luminance: 0.235,
#  in_srgb: True, in_p3: True, in_rec2020: True}
const info = hl.metric.info('#3b82f6');
// {hex, srgb, xyz, lab, L, C, H, luminance, inSrgb, inP3, inRec2020}

metric.distance()

Compressed perceptual distance using MetricSpace with Minkowski exponent and monotonic compression — STRESS 22.48 on COMBVD vs CIEDE2000's 29.2. The Lab-level twin of metric.difference(). Note: compression saturates near ~0.15 for very dissimilar pairs — relative ordering is preserved but absolute magnitudes plateau.

Input contract (1.0): metric.distance(lab_1, lab_2) takes MetricLab values in both Python and JS — the 0.x Python-XYZ/JS-Lab asymmetry is gone. Labs must come from metric.from_hex / metric.from_xyz; passing a GenLab throws. The XYZ-in variant lives only on the raw MetricSpace class.

# Takes Metric Lab values (from metric.from_hex / metric.from_xyz)
lab1 = hl.metric.from_hex("#ff0000")
lab2 = hl.metric.from_hex("#00ff00")
dist = hl.metric.distance(lab1, lab2)
# → 0.1482
// Takes MetricLab values (from metric.fromHex)
const lab1 = hl.metric.fromHex('#ff0000');
const lab2 = hl.metric.fromHex('#00ff00');
const dist = hl.metric.distance(lab1, lab2);
// → 0.1482

gen.fromSrgb() / gen.toSrgb()

Convert between sRGB [0,1] and GenSpace Lab. Use these when you need raw Lab values for generation workflows.

# sRGB → Gen Lab
lab = hl.gen.from_srgb([0.231, 0.510, 0.965])

# Gen Lab → sRGB
rgb = hl.gen.to_srgb(lab)

# Hex shortcuts
lab = hl.gen.from_hex("#3b82f6")
hex_str = hl.gen.to_hex(lab)
// sRGB → Gen Lab
const lab = hl.gen.fromSrgb([0.231, 0.510, 0.965]);

// Gen Lab → sRGB
const rgb = hl.gen.toSrgb(lab);

// Hex shortcuts
const lab2 = hl.gen.fromHex('#3b82f6');
const hex = hl.gen.toHex(lab2);

gen.toLch() / gen.fromLch()

Convert Gen Lab to/from cylindrical LCh (h in degrees, 0–360) — the same coordinates as the helmgenlch space on Color.js. For hue rotations and harmonies you can also use the one-liners gen.rotateHue() and gen.harmonies().

# Gen Lab → [L, C, h°]
lch = hl.gen.to_lch(hl.gen.from_hex("#3b82f6"))
# → [0.5586, 0.2976, 263.1]

# Rotate hue 120° (triadic), back to Lab → hex
lch[2] = (lch[2] + 120) % 360
triad = hl.gen.to_hex(hl.gen.from_lch(lch))

# Or simply:
triad = hl.gen.rotate_hue("#3b82f6", 120)
// Gen Lab → [L, C, h°]
const lch = hl.gen.toLch(hl.gen.fromHex('#3b82f6'));
// → [0.5586, 0.2976, 263.1]

// Rotate hue 120° (triadic), back to Lab → hex
const triad = hl.gen.toHex(hl.gen.fromLch([lch[0], lch[1], (lch[2] + 120) % 360]));

// Or simply:
const triad2 = hl.gen.rotateHue('#3b82f6', 120);

metric.toCss() / metric.inGamut()

Convert Metric Lab to wide-gamut CSS strings with toCss(lab, gamut). It performs proper gamut mapping (preserving lightness and hue, reducing chroma) so colors outside the target gamut land cleanly inside it. inGamut(lab, gamut) tests membership.

lab = hl.metric.from_hex("#ff0000")

p3_css = hl.metric.to_css(lab, gamut="display-p3")
# → 'color(display-p3 0.9176 0.2003 0.1386)'

rec_css = hl.metric.to_css(lab, gamut="rec2020")
# → 'color(rec2020 0.7920 0.2310 0.0738)'

# Gamut tests
hl.metric.in_gamut(lab)                    # sRGB, True/False
hl.metric.in_gamut(lab, "display-p3")
hl.metric.in_gamut(lab, "rec2020")
const lab = hl.metric.fromHex('#ff0000');

const p3Css = hl.metric.toCss(lab, 'display-p3');
// → 'color(display-p3 0.9176 0.2003 0.1386)'

const recCss = hl.metric.toCss(lab, 'rec2020');
// → 'color(rec2020 0.7920 0.2310 0.0738)'

// Gamut tests
hl.metric.inGamut(lab);                  // sRGB, true/false
hl.metric.inGamut(lab, 'display-p3');
hl.metric.inGamut(lab, 'rec2020');

gen.hueRing()

Generate a hue ring at fixed lightness and chroma. Useful for creating categorical color schemes.

# 12 evenly-spaced hues at L=0.6, C=0.15
hues = hl.gen.hue_ring(12, lightness=0.6, chroma=0.15)
// 12 evenly-spaced hues at L=0.6, C=0.15
const hues = hl.gen.hueRing(12, { lightness: 0.6, chroma: 0.15 });

metric.fromXyz() / metric.toXyz()

Direct CIE XYZ conversion via the MetricSpace pipeline.

# CIE XYZ → Metric Lab
lab = hl.metric.from_xyz([0.9505, 1.0, 1.089])

# Metric Lab → CIE XYZ
xyz = hl.metric.to_xyz(lab)
// CIE XYZ → Metric Lab
const lab = hl.metric.fromXyz([0.9505, 1.0, 1.089]);

// Metric Lab → CIE XYZ
const xyz = hl.metric.toXyz(lab);

Using with Color.js

Pending next Color.js release

Helmlab was merged into color-js/color.js master in April 2026. The current published colorjs.io@0.6.1 does not include it yet — install from git or wait for the next release. Use the helmlab package above for production today.

# install from git until the next Color.js release ships
npm install github:color-js/color.js

Color.js is the standard library many design tools and component systems already use. Once the next release lands, you'll be able to convert any color to "helmgen" or "helmlab-metric" the same way you do with "oklab".

Three spaces and one custom deltaE method are registered on Color.js master — same one-line API as oklab / lch:

Space ID Coords Use for
helmgen L, a, b GenSpace — palettes, gradients, gamut mapping
helmgenlch L, C, h GenSpace in cylindrical (LCh) form
helmlab-metric L, a, b MetricSpace — perceptual distance

Plus a custom deltaE method "Helmlab" implementing the v21 pair-dependent SL/SC weighting (STRESS 22.48 on COMBVD).

import Color from "colorjs.io";

// Construct directly in any helm space
const blue = new Color("helmgen", [0.55, -0.04, -0.18]);

// Convert from sRGB / hex / any other space
const hg = new Color("#3b82f6").to("helmgen");

// Smooth gradient — uses GenSpace for interpolation
const grad = blue.steps(new Color("white"), { space: "helmgen", steps: 32 });

// Perceptual distance — pair-dependent MetricSpace deltaE (v21)
const d = new Color("red").deltaE("green", "Helmlab");

// LCh form — useful for hue rotations
const rotated = new Color("helmgenlch", [0.6, 0.18, 240]);
rotated.set("helmgenlch.h", h => h + 30);

Color.js is JavaScript-only. For Python use the helmlab package directly (same math, identical output).

The math is identical to the standalone helmlab package. Pick whichever fits your stack: helmlab if you want a tiny zero-dep bundle and the high-level helpers (gen.palette, gen.gradient, gen.scale), Color.js if you already use it for parsing, gamut mapping, and multi-space conversions.

AI Skills

Teach your AI assistant to get color right. Two agent skills, installable into Claude Code, Codex, Gemini CLI or Cursor with one command:

npx skills add Grkmyldz148/color-skills

color-space-routing

Task → space routing with measured evidence, Lab-vs-LCh form rules, pitfalls and a graveyard of measured-dead approaches. Vendor-neutral: routes to OKLab, CIELAB, CAM16, Jzazbz or Helmlab wherever each wins. Benchmarked: lifts claude-haiku 40%→93% and sonnet 42%→88% on 20 auto-verifiable color tasks.

helmlab

Correct API usage of this library — the two-space model, verified outputs and the exact gotchas that break AI-generated code. Benchmarked: haiku 8%→75%, sonnet 38%→92% on 8 API tasks. Without it, models hallucinate the API.

All numbers from fresh headless sessions, 3 reps per task; the harness is validated with golden answers and raw per-run outputs are committed — github.com/Grkmyldz148/color-skills.

TypeScript

If you use TypeScript, Helmlab ships complete type definitions. Your editor will autocomplete every method and parameter.

Full TypeScript declarations included. ESM and CJS builds. All parameter interfaces are exported for custom configurations.

import type { Lab, GenLab, MetricLab, GenParams, HelmlabParams } from 'helmlab';
import { Helmlab, GenSpace, MetricSpace } from 'helmlab';

// Lab is a [number, number, number] tuple; GenLab / MetricLab are the
// branded variants returned by hl.gen.fromHex / hl.metric.fromHex —
// cross-passing them fails to compile and throws at runtime
const hl = new Helmlab();
const gLab: GenLab = hl.gen.fromHex('#3b82f6');
const mLab: MetricLab = hl.metric.fromHex('#3b82f6');

// GenParams / HelmlabParams (metric) for custom spaces
const params: GenParams = {
  M1: [[...], [...], [...]],
  M2: [[...], [...], [...]],
  alpha: 0.021,
  chroma_power: 0.978,
  // ... other parameters
};

Python type hints are available via py.typed marker. Works with mypy and pyright.

Advanced

Custom Parameters

Both GenSpace and MetricSpace accept custom parameter objects. Use this for research or when training on your own datasets.

from helmlab.spaces.gen import GenSpace, GenParams

# Load custom parameters
params = GenParams.load("my_params.json")

gen = GenSpace(params=params)
import { GenSpace, compileGenParams } from 'helmlab';
import params from './my_params.json';

const gen = new GenSpace(compileGenParams(params));

Raw Space Access

For maximum control, use GenSpace or MetricSpace directly instead of the Helmlab wrapper. This gives you access to the raw Lab coordinates without hex conversion or gamut mapping.

from helmlab.spaces.gen import GenSpace
from helmlab.spaces.metric import MetricSpace
from helmlab.utils.srgb_convert import sRGB_to_XYZ

gen = GenSpace()
metric = MetricSpace(neutral_correction=True)  # NC on = Helmlab wrapper default

# Raw spaces take CIE XYZ, not sRGB
xyz = sRGB_to_XYZ([0.2, 0.5, 0.8])
gen_lab = gen.from_XYZ(xyz)
met_lab = metric.from_XYZ(xyz)

# These are different spaces -- different Lab values
print(gen_lab)   # [0.518, -0.056, -0.211]
print(met_lab)   # [0.660, -0.255, -0.199]
import { GenSpace, MetricSpace, srgbToXyz,
  compileGenParams, getDefaultGenParams,
  compileParams, getDefaultParams } from 'helmlab';

// Raw classes take compiled params (the Helmlab wrapper does this for you)
const gen = new GenSpace(compileGenParams(getDefaultGenParams()));
const metric = new MetricSpace(compileParams(getDefaultParams()));  // NC on by default

// Raw spaces take CIE XYZ, not sRGB
const xyz = srgbToXyz([0.2, 0.5, 0.8]);
const genLab = gen.fromXYZ(xyz);
const metLab = metric.fromXYZ(xyz);

// These are different spaces -- different Lab values
console.log(genLab);  // [0.518, -0.056, -0.211]
console.log(metLab);  // [0.660, -0.255, -0.199]

Neutral Correction Toggle

MetricSpace includes a neutral correction (NC) stage that renders grays exactly neutral. The Helmlab wrapper (and the raw JS class) enable it by default; the raw Python class defaults to off. Toggle it explicitly:

# Neutral correction (raw-class default: False)
metric = MetricSpace(neutral_correction=False)
// Neutral correction (raw-class default: true)
const metric = new MetricSpace(compileParams(getDefaultParams()), { neutralCorrection: false });

Token Export

Export your palettes directly to CSS custom properties, Tailwind configs, Swift (iOS), Android colors, or raw JSON. The hl.tokens namespace gives you all formats — colors in, token strings out.

Every hl.tokens method takes color STRINGS ('#rrggbb', color(display-p3 …), …), never Lab arrays — the 0.x Gen-Lab-into-the-exporter footgun is structurally gone. Conversions go through Metric Lab → XYZ → sRGB/P3/OKLab internally.

Single-Color Formats

Convert a color string to any output format.

hl.tokens.css("#3b82f6", format="hex")     # '#3b82f6'
hl.tokens.css("#3b82f6", format="rgb")     # 'rgb(59, 130, 246)'
hl.tokens.css("#3b82f6", format="hsl")     # 'hsl(217, 91%, 60%)'
hl.tokens.css("#3b82f6", format="oklch")   # 'oklch(62.3% 0.1881 259.8)'
hl.tokens.css("#3b82f6", format="p3")      # 'color(display-p3 0.3047 0.5035 0.9337)'
hl.tokens.android("#3b82f6")               # '0xFF3b82f6'
hl.tokens.ios_p3("#3b82f6")                # {r: 0.3047, g: 0.5035, b: 0.9337}
hl.tokens.swift("#3b82f6")                 # 'Color(.displayP3, red: 0.3047, ...)'
hl.tokens.css('#3b82f6', 'hex');     // '#3b82f6'
hl.tokens.css('#3b82f6', 'rgb');     // 'rgb(59, 130, 246)'
hl.tokens.css('#3b82f6', 'hsl');     // 'hsl(217, 91%, 60%)'
hl.tokens.css('#3b82f6', 'oklch');   // 'oklch(62.3% 0.1881 259.8)'
hl.tokens.css('#3b82f6', 'p3');      // 'color(display-p3 0.3047 0.5035 0.9337)'
hl.tokens.android('#3b82f6');         // '0xFF3b82f6'
hl.tokens.iosP3('#3b82f6');           // {r: 0.3047, g: 0.5035, b: 0.9337}
hl.tokens.swift('#3b82f6');           // 'Color(.displayP3, red: 0.3047, ...)'

Scale Export

Export an entire scale to CSS custom properties, Tailwind config, or JSON.

scale = hl.gen.scale("#3b82f6")

# CSS custom properties
css = hl.tokens.css_variables(scale, prefix="--primary")
#   --primary-50: #e7efff;
#   --primary-100: #d4e3ff;
#   ...

# Tailwind config dict
tw = hl.tokens.tailwind(scale, name="primary")
# {'primary': {'50': '#e7efff', '100': '#d4e3ff', ...}}

# JSON string (multiple scales)
json_str = hl.tokens.json({"primary": scale, "accent": accent_scale})
const scale = hl.gen.scale('#3b82f6');

// CSS custom properties
const css = hl.tokens.cssVariables(scale, '--primary');
//   --primary-50: #e7efff;
//   --primary-100: #d4e3ff;
//   ...

// Tailwind config
const tw = hl.tokens.tailwind(scale, 'primary');
// {primary: {'50': '#e7efff', '100': '#d4e3ff', ...}}

// JSON string (multiple scales)
const json = hl.tokens.json({ primary: scale, accent: accentScale });