chroma-js
Chroma.js is a zero-dependency color conversion and palette toolkit for JavaScript. It parses CSS colors and numeric channel formats, converts among RGB, HSL, Lab, Lch, OKLab, OKLch, CMYK, temperature, and other spaces, then formats the result for CSS or data output. Its more distinctive half is scale generation: continuous gradients, discrete classes, ColorBrewer palettes, perceptual interpolation, lightness correction, data limits, contrast, and color-difference calculations. It is designed for visualization and color computation, not for styling components or managing design tokens.
Chroma.js remains an excellent practical choice for data visualization and palette work, especially when scales matter as much as conversion. For simple UI color edits use a narrower package, and wait for a post-3.2.0 release before relying on the ESM light entry.
Use it if
- You build charts or maps and need data domains, class breaks, ColorBrewer palettes, and perceptual interpolation in one package
- You need to parse and convert both legacy and modern CSS color forms, including Lab, Lch, OKLab, and OKLch
- You want color utilities such as WCAG contrast, APCA estimates, Delta E 2000, blending, averaging, or color-temperature conversion
- You prefer one mature, zero-dependency color toolkit over assembling several single-purpose conversion packages
- You only need to parse a CSS color and lighten it; Bundlephobia reports 16.2 KB gzipped for the full package, while `color` or `tinycolor2` offers a narrower API
- You need first-party TypeScript declarations; the 3.2.0 manifest has no `types` field and publishes no declaration files, so TypeScript projects depend on separately maintained community types
- You want the advertised ESM light build today; 3.2.0 maps `chroma-js/light` to `index-light.js`, but that file is missing from the published tarball, and the repository fixed it only after the release
- You require old comma-separated CSS output; the changelog marks v3's switch from `rgb(255, 255, 0)` to modern `rgb(255 255 0)` syntax as a breaking change
- You need a standards-backed production APCA decision; the implementation source labels `contrastAPCA` beta and tells users to update regularly because the algorithm can still change
Setup reality
For JavaScript, setup starts and mostly ends with `npm install chroma-js` and `import chroma from 'chroma-js'`. There are no runtime dependencies, peers, native builds, credentials, or configuration files. The full ESM entry and CommonJS build are both exported. The rough edges appear when you optimize or add types. The full build is 16.2 KB gzipped according to Bundlephobia, and version 3.2.0 advertises `chroma-js/light`; however, its ESM target `index-light.js` is absent from the published tarball. `require('chroma-js/light')` has a CommonJS target, but an ESM import fails until a release includes the repository's later missing-file fix. Deep ESM imports under documented `src` paths are exported, but they are lower-level and easy to misuse because much of Chroma's API is installed through module side effects. TypeScript declarations are not bundled, so use separately versioned community declarations or write a small local type boundary and check it against v3. The other migration surprise is output: `color.css()` now emits modern space-separated CSS syntax. Alpha-bearing `.hex()` values default to eight-digit RGBA, so request `.hex('rgb')` when a six-digit result is required. Color methods generally return new values, but scales are callable objects with mutable configuration methods and an internal cache. Decide your interpolation space explicitly, validate untrusted strings with `chroma.valid()`, treat `null` as the scale's no-data value, and add tests for clipped colors, hue-less interpolation, white-point changes, and exact serialized output.
Patterns
Parse a CSS color and choose an output formatparse-and-format
import chroma from 'chroma-js';
const color = chroma('rgba(255, 0, 0, 0.5)');
console.log(color.css()); // rgb(255 0 0 / 0.5)
console.log(color.hex()); // #ff000080
console.log(color.hex('rgb')); // #ff0000
console.log(color.rgba()); // [255, 0, 0, 0.5]Version 3 emits modern CSS syntax, and `.hex()` includes alpha automatically when opacity is below 1. Request `rgb` for a six-digit hex value.
Validate untrusted color text before parsingvalidate-input
import chroma from 'chroma-js';
function normalizeColor(input) {
if (!chroma.valid(input)) return null;
return chroma(input).hex();
}
console.log(normalizeColor('oklch(70% 0.1 30)'));
console.log(normalizeColor('not-a-color')); // nullCalling `chroma(input)` directly can throw for an invalid color. `chroma.valid()` is the intended guard for user-provided values.
Convert between RGB and perceptual spacesconvert-color-spaces
import chroma from 'chroma-js';
const color = chroma('#663399');
const rgb = color.rgb();
const lab = color.lab();
const oklch = color.oklch();
const rebuilt = chroma.oklch(...oklch);
console.log({ rgb, lab, oklch, hex: rebuilt.hex() });Conversions can leave the displayable RGB gamut. Check `.clipped()` after constructing or editing perceptual channels when exact displayability matters.
Adjust lightness, saturation, and opacityadjust-color
import chroma from 'chroma-js';
const base = chroma('#6699cc');
const active = base.darken(0.6).saturate(0.4).alpha(0.85);
const disabled = base.desaturate(1.2).alpha(0.5);
console.log(active.css());
console.log(disabled.css());
console.log(base.hex()); // original is unchangedColor adjustment methods return new color objects. Their numeric amounts are library units, not CSS percentages.
Interpolate two colors in a chosen spacemix-perceptually
import chroma from 'chroma-js';
const midpoint = chroma.mix('#ff0000', '#0000ff', 0.5, 'oklch');
const quarter = chroma.mix('#ff0000', '#0000ff', 0.25, 'lab');
console.log(midpoint.hex());
console.log(quarter.hex());Interpolation mode materially changes the result. Set it explicitly for design or visualization work instead of depending on the default.
Map a numeric domain to a continuous scalemap-numeric-domain
import chroma from 'chroma-js';
const temperature = chroma
.scale(['#2166ac', '#f7f7f7', '#b2182b'])
.domain([-20, 0, 40])
.mode('lab');
console.log(temperature(-8).hex());
console.log(temperature(25).hex());
console.log(temperature.domain()); // [-20, 0, 40] in 3.2.0Version 3.2 changed no-argument `.domain()` to return the original domain array, including intermediate stops, rather than only its endpoints.
Create quantile classes from databuild-classed-scale
import chroma from 'chroma-js';
const values = [3, 4, 4, 8, 12, 19, 21, 80, 110];
const breaks = chroma.limits(values, 'q', 5);
const scale = chroma.scale('YlGnBu').classes(breaks);
const rows = values.map((value) => ({ value, color: scale(value).hex() }));
console.log(breaks, rows);Quantile classes distribute observations, not numeric distance. Duplicate-heavy data can produce repeated breaks, so inspect the returned array before publishing a legend.
Generate a fixed palette with corrected lightnesssample-palette
import chroma from 'chroma-js';
const palette = chroma
.scale(['#440154', '#21918c', '#fde725'])
.mode('lab')
.correctLightness()
.colors(7);
console.log(palette);`.correctLightness()` changes sample positions to make perceived lightness more regular. It is useful for sequential ramps, not automatically appropriate for diverging scales.
Use a built-in ColorBrewer paletteuse-colorbrewer
import chroma from 'chroma-js';
const paletteName = 'RdYlBu';
if (!(paletteName in chroma.brewer)) throw new Error('unknown palette');
const scale = chroma.scale(paletteName).domain([0, 100]);
console.log(scale.colors(9));Chroma's interpolated output is not always identical to an official ColorBrewer class count. Use the exact `chroma.brewer[name]` array when official discrete swatches are required.
Calculate text and background contrastcheck-contrast
import chroma from 'chroma-js';
const foreground = '#222222';
const background = '#ffffff';
const wcagRatio = chroma.contrast(foreground, background);
const apcaEstimate = chroma.contrastAPCA(foreground, background);
console.log({ wcagRatio, apcaEstimate });The source labels APCA support beta and warns that its algorithm may change. Keep WCAG and APCA results distinct and do not treat their numeric scales as interchangeable.
Measure perceptual color differencemeasure-color-difference
import chroma from 'chroma-js';
const expected = '#ff0000';
const measured = '#fe0100';
const deltaE2000 = chroma.deltaE(expected, measured);
const labDistance = chroma.distance(expected, measured, 'lab');
console.log({ deltaE2000, labDistance });`deltaE()` uses Delta E 2000, while `distance()` is Euclidean distance in the selected color space. Their values answer different questions.
Give missing values an explicit scale colorhandle-missing-data
import chroma from 'chroma-js';
const scoreColor = chroma
.scale(['#f7fbff', '#08306b'])
.domain([0, 100])
.nodata('#bdbdbd');
console.log(scoreColor(72).hex());
console.log(scoreColor(null).hex()); // #bdbdbd
console.log(scoreColor(undefined).hex()); // #bdbdbdScales treat `null` and non-numeric values as no data. Set `.nodata()` deliberately so missing values cannot masquerade as the low end of a ramp.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| culori | npm | Choose it for a more modular, modern color-space toolkit with tree-shakable functions and broad CSS Color support |
| color | npm | Choose it for straightforward immutable CSS color parsing, conversion, and adjustment without Chroma's data-scale machinery |
| tinycolor2 | npm | Choose it for familiar parsing and basic manipulation in older browser-oriented projects that do not need perceptual scales |