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 (~12KB 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
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 wrapping both GenSpace (generation) and MetricSpace (measurement). It accepts hex strings, returns hex strings, and handles gamut mapping internally.
from helmlab import Helmlab
hl = Helmlab()
# Generate a 10-shade palette
palette = hl.palette("#3b82f6", steps=10)
# Smooth gradient from red to blue (16 steps)
gradient = hl.gradient("#ef4444", "#3b82f6", steps=16)
# Perceptual color distance
dist = hl.delta_e("#ff0000", "#00ff00")
# Ensure WCAG AA contrast (adjusts color if needed)
adjusted = hl.ensure_contrast("#3b82f6", "#ffffff", min_ratio=4.5)
# Tailwind-style semantic scale
scale = hl.semantic_scale("#3b82f6") import { Helmlab } from 'helmlab';
const hl = new Helmlab();
// Generate a 10-shade palette
const palette = hl.palette('#3b82f6', 10);
// Smooth gradient from red to blue (16 steps)
const gradient = hl.gradient('#ef4444', '#3b82f6', 16);
// Perceptual color distance
const dist = hl.deltaE('#ff0000', '#00ff00');
// Ensure WCAG AA contrast (adjusts color if needed)
const adjusted = hl.ensureContrast('#3b82f6', '#ffffff', 4.5);
// Tailwind-style semantic scale
const scale = hl.semanticScale('#3b82f6'); GenSpace
GenSpace is the color space optimized for creating colors -- gradients, palettes, and gamut mapping. It produces smooth, visually pleasing gradients without the lavender shift that plagues OKLab in the blue region.
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 → C^cp → PW_L → enrichment → Lab. All transforms have exact analytical inverses.
Forward / Inverse Transform
from helmlab.spaces.gen import GenSpace
gen = GenSpace()
# sRGB [0,1] → Helmlab Lab [L, a, b]
lab = gen.from_srgb([0.231, 0.510, 0.965])
# Helmlab Lab → sRGB [0,1]
rgb = gen.to_srgb(lab)
# XYZ → Lab (lower level)
lab = gen.forward(xyz)
xyz = gen.inverse(lab) import { GenSpace } from 'helmlab';
const gen = new GenSpace();
// sRGB [0,1] → Helmlab Lab [L, a, b]
const lab = gen.fromSrgb([0.231, 0.510, 0.965]);
// Helmlab Lab → sRGB [0,1]
const rgb = gen.toSrgb(lab);
// XYZ → Lab (lower level)
const lab2 = gen.forward(xyz);
const xyz2 = gen.inverse(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.gradient("#ef4444", "#3b82f6", steps=16)
# Returns: ['#ef4444', '#d4538d', '#b560c1', ..., '#3b82f6'] import { Helmlab } from 'helmlab';
const hl = new Helmlab();
// 16-step gradient with arc-length reparameterization
const grad = hl.gradient('#ef4444', '#3b82f6', 16);
// Returns: ['#ef4444', '#d4538d', '#b560c1', ..., '#3b82f6'] Palette Generation
# Generate a 10-shade palette (dark to light)
palette = hl.palette("#3b82f6", steps=10)
# Returns: ['#0a1628', '#132d50', ..., '#3b82f6', ..., '#dbe8fd'] // Generate a 10-shade palette (dark to light)
const palette = hl.palette('#3b82f6', 10);
// Returns: ['#0a1628', '#132d50', ..., '#3b82f6', ..., '#dbe8fd'] Key properties: 360/360/360 valid cusps across sRGB, Display P3, and Rec.2020. Zero monotonicity violations. Blue-to-white gradient produces sky blue midpoint (G/R = 1.51), not lavender. 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.18. Pipeline: XYZ → M1 → γ → M2 → Hue → H-K → L → C → HL → NC → φ → Lab.
Color Difference
from helmlab import Helmlab
hl = Helmlab()
# Perceptual distance between two colors
dist = hl.delta_e("#ff0000", "#00ff00")
# → 0.423 (lower = more similar)
# Contrast ratio (WCAG-compatible)
ratio = hl.contrast_ratio("#ffffff", "#3b82f6")
# → 3.68 import { Helmlab } from 'helmlab';
const hl = new Helmlab();
// Perceptual distance between two colors
const dist = hl.deltaE('#ff0000', '#00ff00');
// → 0.423 (lower = more similar)
// Contrast ratio (WCAG-compatible)
const ratio = hl.contrastRatio('#ffffff', '#3b82f6');
// → 3.68 STRESS Evaluation
from helmlab.spaces.metric import MetricSpace
metric = MetricSpace()
# Forward transform: XYZ → MetricSpace Lab
lab = metric.forward(xyz)
# Distance with the full enriched formula
dist = metric.distance(lab1, lab2) import { MetricSpace } from 'helmlab';
const metric = new MetricSpace();
// Forward transform: XYZ → MetricSpace Lab
const lab = metric.forward(xyz);
// Distance with the full enriched formula
const dist = metric.distance(lab1, lab2); Benchmark: STRESS 22.48 on COMBVD (with Bradford CAT). Beats CIEDE2000 (29.18), CIE94 (33.59), CAM16-UCS (33.90), and all other published formulas. 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 wraps both GenSpace and MetricSpace and provides a clean hex-in, hex-out API.
High-level API class. Accepts hex strings, handles sRGB conversion and gamut mapping internally. Uses GenSpace for generation methods and MetricSpace for distance/contrast methods.
palette()
Generate a lightness-uniform palette from a single color.
palette = hl.palette("#3b82f6", steps=10)
# Returns list of hex strings: ['#0a1628', '#132d50', ...] const palette = hl.palette('#3b82f6', 10);
// Returns string[]: ['#0a1628', '#132d50', ...] gradient()
Generate a perceptually uniform gradient between two colors using CIEDE2000 arc-length reparameterization.
grad = hl.gradient("#ef4444", "#3b82f6", steps=16)
# Steps are equally spaced in CIEDE2000 perceptual distance const grad = hl.gradient('#ef4444', '#3b82f6', 16);
// Steps are equally spaced in CIEDE2000 perceptual distance semanticScale()
Generate a Tailwind-style 50-950 semantic scale from a base color.
scale = hl.semantic_scale("#3b82f6")
# Returns dict: {50: '#eff6ff', 100: '#dbeafe', ..., 950: '#0a1628'} const scale = hl.semanticScale('#3b82f6');
// Returns Record<number, string>: {50: '#eff6ff', 100: '#dbeafe', ...} adaptToMode()
Adapt a color between light and dark mode via soft L-inversion in GenSpace.
# Light → dark
dark = hl.adapt_to_mode("#3b82f6", from_mode="light", to_mode="dark")
# Dark → light
light = hl.adapt_to_mode("#93c5fd", from_mode="dark", to_mode="light") // Light → dark
const dark = hl.adaptToMode('#3b82f6', 'light', 'dark');
// Dark → light
const light = hl.adaptToMode('#93c5fd', 'dark', 'light'); ensureContrast()
Adjust a color to meet a minimum WCAG contrast ratio against a background.
# Ensure WCAG AA (4.5:1) contrast
adjusted = hl.ensure_contrast("#3b82f6", "#ffffff", min_ratio=4.5)
# Ensure WCAG AAA (7:1) contrast
adjusted = hl.ensure_contrast("#3b82f6", "#ffffff", min_ratio=7.0) // Ensure WCAG AA (4.5:1) contrast
const adjusted = hl.ensureContrast('#3b82f6', '#ffffff', 4.5);
// Ensure WCAG AAA (7:1) contrast
const adjusted2 = hl.ensureContrast('#3b82f6', '#ffffff', 7.0); meetsContrast()
Check if a foreground/background pair meets a WCAG contrast level without modifying the colors.
hl.meets_contrast("#3b82f6", "#ffffff", level="AA") # False
hl.meets_contrast("#1e40af", "#ffffff", level="AAA") # True hl.meetsContrast('#3b82f6', '#ffffff', 'AA'); // false
hl.meetsContrast('#1e40af', '#ffffff', 'AAA'); // true 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.adapt_pair(
"#3366ff", "#ffffff",
from_mode="light", to_mode="dark", min_ratio=4.5
) // Returns [fgHex, bgHex] with contrast ≥ 4.5
const [fg, bg] = hl.adaptPair('#3366ff', '#ffffff', 'light', 'dark', 4.5); info()
Get detailed color information: hex, sRGB, XYZ, Lab (metric), L/C/H, and relative luminance.
info = hl.info("#3b82f6")
# {hex: '#3b82f6', srgb: [0.231, 0.510, 0.965],
# xyz: [0.242, 0.174, 0.967], lab: [0.57, -0.05, -0.18],
# L: 0.57, C: 0.19, H: 254.6, luminance: 0.174} const info = hl.info('#3b82f6');
// {hex, srgb, xyz, lab, L, C, H, luminance} perceptualDistance()
Full perceptual distance using MetricSpace with Minkowski exponent and compression. More accurate than deltaE() which uses simple Euclidean distance.
# Takes Metric Lab values (from from_hex / from_XYZ)
lab1 = hl.from_hex("#ff0000")
lab2 = hl.from_hex("#00ff00")
dist = hl.perceptual_distance(lab1, lab2)
# → 0.1485 // Takes Metric Lab values (from fromHex)
const lab1 = hl.fromHex('#ff0000');
const lab2 = hl.fromHex('#00ff00');
const dist = hl.perceptualDistance(lab1, lab2);
// → 0.1485 genFromSrgb() / genToSrgb()
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.genFromSrgb([0.231, 0.510, 0.965]);
// Gen Lab → sRGB
const rgb = hl.genToSrgb(lab);
// Hex shortcuts
const lab2 = hl.genFromHex('#3b82f6');
const hex = hl.genToHex(lab2); toHexP3()
Convert Metric Lab to a CSS color(display-p3 r g b) string for wide-gamut displays.
lab = hl.from_hex("#ff0000")
p3_css = hl.to_hex_p3(lab)
# → 'color(display-p3 0.9176 0.2003 0.1386)' const lab = hl.fromHex('#ff0000');
const p3Css = hl.toHexP3(lab);
// → 'color(display-p3 0.9176 0.2003 0.1386)' paletteHues()
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.palette_hues(lightness=0.6, chroma=0.15, steps=12) // 12 evenly-spaced hues at L=0.6, C=0.15
const hues = hl.paletteHues(0.6, 0.15, 12); fromXYZ() / toXYZ()
Direct CIE XYZ conversion via the MetricSpace pipeline.
# CIE XYZ → Helmlab Lab (metric pipeline)
lab = hl.from_XYZ([0.9505, 1.0, 1.089])
# Helmlab Lab → CIE XYZ
xyz = hl.to_XYZ(lab) // Use MetricSpace directly for XYZ access
import { MetricSpace } from 'helmlab';
const metric = new MetricSpace();
const lab = metric.fromXYZ([0.9505, 1.0, 1.089]);
const xyz = metric.toXYZ(lab); 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, GenParams, MetricParams } from 'helmlab';
import { Helmlab, GenSpace, MetricSpace } from 'helmlab';
// Lab is a [number, number, number] tuple
const color: Lab = [0.5, -0.1, 0.2];
// GenParams / MetricParams 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.
import json
from helmlab.spaces.gen import GenSpace
# Load custom parameters
with open("my_params.json") as f:
params = json.load(f)
gen = GenSpace(params=params) import { GenSpace } from 'helmlab';
import params from './my_params.json';
const gen = new GenSpace(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
gen = GenSpace()
metric = MetricSpace()
# Convert sRGB to Lab (both spaces)
gen_lab = gen.from_srgb([0.2, 0.5, 0.8])
met_lab = metric.from_srgb([0.2, 0.5, 0.8])
# These are different spaces -- different Lab values
print(gen_lab) # [0.584, -0.087, -0.194]
print(met_lab) # [0.571, -0.052, -0.183] import { GenSpace, MetricSpace } from 'helmlab';
const gen = new GenSpace();
const metric = new MetricSpace();
// Convert sRGB to Lab (both spaces)
const genLab = gen.fromSrgb([0.2, 0.5, 0.8]);
const metLab = metric.fromSrgb([0.2, 0.5, 0.8]);
// These are different spaces -- different Lab values
console.log(genLab); // [0.584, -0.087, -0.194]
console.log(metLab); // [0.571, -0.052, -0.183] Neutral Correction Toggle
MetricSpace includes a neutral correction (NC) stage that improves STRESS on achromatic and near-achromatic pairs. You can disable it if needed.
# Disable neutral correction
metric = MetricSpace(enable_nc=False) // Disable neutral correction
const metric = new MetricSpace({ enableNc: false }); Token Export
Export your palettes directly to CSS custom properties, Tailwind configs, Swift (iOS), Android colors, or raw JSON. The export() method on the Helmlab class gives you a TokenExporter with all formats.
hl.export() returns a TokenExporter instance bound to the Helmlab's GenSpace. All conversions go through Gen Lab → sRGB/P3/OKLab internally.
Single-Color Formats
Convert a Gen Lab value to any output format.
ex = hl.export()
lab = hl.gen_from_hex("#3b82f6")
ex.to_css_hex(lab) # '#3b82f6'
ex.to_css_rgb(lab) # 'rgb(59, 130, 246)'
ex.to_css_hsl(lab) # 'hsl(217, 91%, 60%)'
ex.to_css_oklch(lab) # 'oklch(61.0% 0.197 254.6)'
ex.to_css_display_p3(lab) # 'color(display-p3 0.310 0.519 0.945)'
ex.to_android_argb(lab) # '0xFF3b82f6'
ex.to_ios_p3(lab) # {r: 0.310, g: 0.519, b: 0.945}
ex.to_swift_literal(lab) # 'Color(.displayP3, red: 0.310, ...)' const ex = hl.export();
const lab = hl.genFromHex('#3b82f6');
ex.toCssHex(lab); // '#3b82f6'
ex.toCssRgb(lab); // 'rgb(59, 130, 246)'
ex.toCssHsl(lab); // 'hsl(217, 91%, 60%)'
ex.toCssOklch(lab); // 'oklch(61.0% 0.197 254.6)'
ex.toCssDisplayP3(lab); // 'color(display-p3 0.310 0.519 0.945)'
ex.toAndroidArgb(lab); // '0xFF3b82f6'
ex.toIosP3(lab); // {r: 0.310, g: 0.519, b: 0.945}
ex.toSwiftLiteral(lab); // 'Color(.displayP3, red: 0.310, ...)' Scale Export
Export an entire scale to CSS custom properties, Tailwind config, or JSON.
scale = hl.semantic_scale("#3b82f6")
# CSS custom properties
css = ex.export_css_custom_properties(scale, prefix="primary")
# primary-50: #e5edff;
# primary-100: #d1dfff;
# ...
# Tailwind config dict
tw = ex.export_tailwind(scale, name="primary")
# {'primary': {'50': '#e5edff', '100': '#d1dfff', ...}}
# JSON string (multiple scales)
json_str = ex.export_json({"primary": scale, "accent": accent_scale}) const scale = hl.semanticScale('#3b82f6');
const scaleArr = Object.values(scale);
// CSS custom properties
const css = ex.exportCssCustomProperties(scaleArr, 'primary');
// primary-0: #e5edff;
// primary-1: #d1dfff;
// ...
// Tailwind config
const tw = ex.exportTailwind(scaleArr, 'primary');
// {primary: {0: '#e5edff', 1: '#d1dfff', ...}}
// JSON string (multiple scales)
const json = ex.exportJson({ primary: scaleArr, accent: accentArr });