color
color is a small JavaScript library for reading a colour in one format, converting it to another, and nudging it around. You hand the Color() function almost anything a stylesheet would accept, a hex string, an rgb() or hsl() string, a CSS keyword, or a plain object like {r, g, b}, and you get back an immutable object. From there you chain: .lighten(0.2).rotate(45).alpha(0.8) each return a brand new frozen instance, and .hex(), .string(), .array(), or .object() get a value back out. It also carries the WCAG bits people actually need, .luminosity(), .contrast(other), .isDark(), so you can decide whether text on a background should be black or white.
Still the most convenient way to take a CSS colour string, convert it, and check contrast, and the immutable v5 API is pleasant. Choose culori or colord instead if you need oklch and wide-gamut CSS, or if you are stuck on CommonJS.
Use it if
- You take colour input as CSS strings from users, a theme file, or a CMS and need to parse hex, rgb(), hsl(), and keywords without writing regexes
- You need WCAG contrast checks or an isDark test to pick readable text colour over an arbitrary background
- You build terminal tooling and want rgb converted to ansi16 or ansi256 codes, which the same object handles
- You want a chainable, immutable API for tweaks such as lighten, darken, rotate, fade, and mix rather than hand-rolling HSL arithmetic
- You are on CommonJS: version 5 is ESM only, so require('color') throws, and CJS projects are pinned to 4.2.3 from April 2022
- You work in modern CSS colour: it cannot parse oklch(), lch(), or color(display-p3 ...) strings at all, and while .oklch() converts numerically, calling .string() on it falls back to rgb() output
- You need perceptually sane manipulation: lighten and darken scale HSL lightness, so lighten() on a colour at 0% lightness returns the same black, and mix() interpolates in sRGB, which muddies midpoints compared to oklab blending in culori
- You only convert between two formats: 8.2 KB gzipped and three transitive dependencies is a lot next to color-convert on its own, or ten lines of hex parsing
- You need an active project: 4.2.3 to 5.0.0 was a three-year gap, the last push was November 2025, and there is one maintainer
Setup reality
npm install color and that is genuinely it, no peer dependencies and no native build. The friction is packaging. Version 5 declares type: module with a single ./index.js export and Node 18 or newer, so require() fails and any Jest or bundler setup that still transpiles to CommonJS needs ESM handling before your first import works. Version 5 also ships its own index.d.ts, so the separate @types/color package is now redundant and will slowly drift from reality; remove it. Instances are frozen, which means every mutation returns a new object and code that assigned to properties in older versions silently stops working, or throws in strict mode. Invalid input throws rather than returning null, so anything coming from a user needs a try/catch around the constructor.
Patterns
Read one format, write anotherparse-and-convert
import Color from 'color';
const c = Color('#7743CE');
c.rgb().array(); // [ 119, 67, 206 ]
c.hsl().string(); // 'hsl(262.4, 58.6%, 53.5%)'
c.cmyk().round().array(); // [ 42, 67, 0, 19 ]The object remembers which model it came from, so .object() and .array() reflect the current model, not always rgb. Call .rgb() first if you want rgb output guaranteed.
Handle colour strings you do not controlsafe-parse-user-input
function parseColor(input) {
try {
return Color(input);
} catch {
return null; // 'Unable to parse color from string'
}
}The constructor throws on anything it cannot read, including valid modern CSS such as oklch(0.6 0.2 30). There is no isValid() helper, so try/catch is the only option.
Remember that every call returns a new objectimmutable-chaining
const base = Color('red');
const brighter = base.green(100);
base.hex(); // '#FF0000' unchanged
brighter.hex(); // '#FF6400'
Object.isFrozen(base); // trueInstances are frozen in v5, so nothing mutates in place and you must keep the return value. Code written against older versions that relied on chaining for side effects quietly does nothing.
Get hex output that keeps transparencyhex-with-alpha
const c = Color('#FF000080');
c.hex(); // '#FF0000' alpha dropped
c.hexa(); // '#FF000080' 8-digit hex
c.alpha(); // 0.5019607843137255hex() silently discards alpha, which is the most common source of a colour looking right in dev tools and wrong in the app. Use hexa() or rgb().string() when transparency matters.
Pick readable text colour for a backgroundcontrast-and-readable-text
const bg = Color(userTheme.background);
const text = bg.isLight() ? '#000000' : '#FFFFFF';
const ratio = bg.contrast(Color(text));
if (ratio < 4.5) {
console.warn(`contrast ${ratio.toFixed(2)} fails WCAG AA for body text`);
}contrast() returns the WCAG ratio from 1 to 21; AA wants 4.5 for body text and 3 for large text. isLight() uses a YIQ approximation, not the same maths as contrast(), so the two can disagree at the edges.
Build hover and active variantslighten-and-darken
const brand = Color('#2E7D32');
const hover = brand.lighten(0.15).hex();
const active = brand.darken(0.15).hex();These are multiplicative on HSL lightness, not additive. Color('hsl(100, 50%, 0%)').lighten(0.5) is still 0% lightness, so pure black never lightens; use .lightness(n) to set an absolute value instead.
Blend colours togethermix-two-colors
Color('cyan').mix(Color('yellow')).hex(); // '#80FF80'
Color('cyan').mix(Color('yellow'), 0.3).hex(); // 30% toward yellowMixing happens in sRGB, so midpoints of saturated complements come out greyer than a perceptual space would give. For gradients and scales, chroma-js or culori interpolate in lab or oklab.
Work with transparencyalpha-and-fading
const overlay = Color('#000').alpha(0.6).rgb().string();
// 'rgba(0, 0, 0, 0.6)'
Color('rgba(10, 10, 10, 0.8)').fade(0.5).alpha(); // 0.4
Color('rgba(10, 10, 10, 0.8)').opaquer(0.5).alpha(); // 1alpha(n) sets an absolute value, while fade and opaquer scale the existing one by a ratio, so fade(0.5) on 0.8 gives 0.4, not 0.3. Alpha survives conversion between models.
Control rounding in CSS outputcontrol-string-precision
const c = Color('#7743CE').lighten(0.5);
c.hsl().string(); // one decimal place by default
c.hsl().string(0); // 'hsl(262, 59%, 80%)'
c.round().object(); // integer channel valuesstring() takes a decimal-place count and defaults to 1, which is why generated CSS often carries values like 58.6%. Pass 0 when the output goes into a design token file people have to read.
Convert a colour to terminal codesterminal-ansi-codes
Color('#7743CE').ansi256().object(); // { ansi256: 98 }
Color('#7743CE').ansi16().object(); // { ansi16: 95 }Handy for CLI tools that accept hex in config but must emit ANSI escapes. The mapping is lossy in one direction only: rgb to ansi256 quantises, and converting back gives a different hex.
Know what oklch does and does not do hereoklch-conversion-limits
Color('#ff0000').oklch().round(3).array(); // [ 62.796, 25.768, 29.234 ]
Color('#ff0000').oklch().string(); // 'rgb(255, 0, 0)'
Color('oklch(0.6 0.2 30)'); // throws: unable to parseThe conversion exists through color-convert, but there is no CSS oklch parser and no oklch serializer, so strings round-trip through rgb. If oklch is your working space, use culori.
Read and set individual channelschannel-getters-setters
const c = Color('hsl(200, 50%, 40%)');
c.hue(); // 200
c.saturationl(80).hsl().string();
c.rotate(180).hue(); // 20
c.grayscale().hex();Calling a channel with no argument reads it, with an argument returns a new colour. hue() wraps modulo 360, so rotate(-90) and rotate(270) are the same, and saturationl versus saturationv is the usual typo.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| culori | npm | You need CSS Color 4 spaces such as oklch, display-p3, and proper gamut mapping, with tree-shakeable imports |
| colord | npm | You want a much smaller immutable library with a similar chainable feel and optional plugins for the extras |
| chroma-js | npm | Your real job is scales, interpolation, and palettes for data visualisation rather than single-colour tweaks |