colord
colord parses color strings and objects, converts between color models, and does the everyday manipulations: lighten, darken, saturate, set alpha, rotate hue, check contrast. The whole thing is one chainable, immutable function, so colord("#ff0000").darken(0.2).alpha(0.5).toRgbString() reads left to right and never mutates the value you started with. Only hex, RGB, HSL and HSV live in the core; CSS color names, WCAG contrast, mixing, LAB, LCH, HWB, CMYK, XYZ and harmonies are opt-in plugins you register once with extend(). That split is why the core stays near 2 KB gzipped with zero dependencies, and it ships its own TypeScript types.
The best value per byte for hex, RGB and HSL conversion plus WCAG contrast in a browser bundle, and the immutable chained API is genuinely pleasant. The four-year release gap and the total absence of OKLCH mean it is a poor foundation for a new design-token system.
Use it if
- You need hex to RGB to HSL conversion plus lighten and darken inside a browser bundle, and you care that it costs about 2 KB gzipped instead of the roughly 16 KB chroma-js adds
- You want WCAG contrast checks in product code: the a11y plugin gives contrast(), isReadable() with AA and AAA levels, and luminance() without pulling in an accessibility toolkit
- You want immutability by construction, so a color passed down a component tree cannot be changed under you: every method returns a new Colord instance
- You are replacing tinycolor2 or color in an existing codebase and want a near-identical method vocabulary with real TypeScript types included in the package
- You need modern color spaces. There is no OKLCH, no OKLab, no display-p3, and no CSS Color Level 4 color() support at all. If your design tokens are written in oklch(), colord cannot read them
- You cannot live with a frozen package. The last npm release is 2.9.3 from August 2022. The repo saw commits as recently as May 2026, but nothing has shipped in almost four years, so treat any bug you find as permanent
- You are generating scales, gradients, or interpolated palettes. colord mixes two colors at a time; chroma-js and culori are built for multi-stop scales and perceptual interpolation
- Invalid input does not throw. colord("abracadabra") quietly becomes black and you only find out if you call isValid() yourself, which is a bad default for anything ingesting user or CMS data
- You write a shared library. extend() mutates a module-level prototype for the whole process, so registering a plugin inside your package silently changes behavior for the host application
Setup reality
npm install colord and you are done: zero dependencies, ESM and CommonJS builds, TypeScript types in the package, no peer deps and no build step. The friction is entirely the plugin system. Anything outside hex, RGB, HSL and HSV needs a separate import passed to extend() exactly once at application startup, before any call that uses it. Two traps follow from that. First, TypeScript stops complaining the moment you import the plugin module, whether or not extend() actually ran, so a missing extend() surfaces at runtime as "TypeError: colord(...).contrast is not a function". Second, extend() writes onto a shared prototype, so plugin registration is global state: fine in an app entrypoint, wrong in a library. Also note the plugin method values are computed by colord's own ports of the CSS Color 4 conversion code, so LAB and LCH numbers can differ in the second decimal from what the README shows.
Patterns
Parse any supported input and convert between modelsparse-and-convert
import { colord } from "colord";
colord("#ff0000").toRgb(); // { r: 255, g: 0, b: 0, a: 1 }
colord("hsl(0, 100%, 50%)").toHex(); // "#ff0000"
colord({ r: 255, g: 0, b: 0 }).toHslString(); // "hsl(0, 100%, 50%)"
colord("rgba(0, 0, 0, 0.5)").toHsv();Core input formats are hex (3, 4, 6 and 8 digit), rgb/rgba, hsl/hsla and hsv objects. Named colors like "tomato" need the names plugin; without it they parse as invalid.
Check a color before trusting itvalidate-untrusted-input
import { colord, getFormat } from "colord";
colord("#wwuutt").isValid(); // false
colord("#wwuutt").toHex(); // "#000000" <- silent fallback
getFormat("#aabbcc"); // "hex"
getFormat("nope"); // undefinedThis is the biggest footgun in the library: bad input never throws, it becomes black. Gate every externally sourced color on isValid() or getFormat() before you render it.
Lighten, darken, saturate, grayscaleadjust-lightness-and-saturation
import { colord } from "colord";
colord("#223344").lighten(0.3).toHex(); // "#5580aa"
colord("#5580aa").darken(0.3).toHex(); // "#223344"
colord("#bf4040").saturate(0.25).toHex(); // "#df2020"
colord("#bf4040").grayscale().toHex(); // "#808080"These operate on HSL lightness and saturation, not perceptual lightness, so lighten(0.3) on a saturated blue and on a yellow do not look equally lighter. Use the lab plugin if you need perceptual results.
Read and set alpha and huealpha-and-hue
import { colord } from "colord";
colord("rgb(0, 0, 0)").alpha(0.5).toRgbString(); // "rgba(0, 0, 0, 0.5)"
colord("rgba(50, 100, 150, 0.5)").alpha(); // 0.5
colord("hsl(90, 50%, 50%)").hue(180).toHslString();
colord("hsl(90, 50%, 50%)").rotate(90).toHslString(); // "hsl(180, 50%, 50%)"alpha() and hue() are getters with no argument and setters with one. Every setter returns a new instance, so const dark = c.darken(0.2) leaves c untouched.
Register plugins once at the app entrypointregister-plugins
// colord-setup.ts, imported first in your entry file
import { extend } from "colord";
import a11yPlugin from "colord/plugins/a11y";
import namesPlugin from "colord/plugins/names";
import mixPlugin from "colord/plugins/mix";
extend([a11yPlugin, namesPlugin, mixPlugin]);extend() patches a shared prototype for the whole process, so call it once in application code and never inside a reusable library. Forgetting it gives "TypeError: colord(...).contrast is not a function" even though TypeScript compiled fine.
Check contrast and readability (a11y plugin)check-wcag-contrast
import { colord, extend } from "colord";
import a11yPlugin from "colord/plugins/a11y";
extend([a11yPlugin]);
colord("#777777").contrast(); // 4.47 against white
colord("#000000").isReadable(); // true (AA, normal text)
colord("#e60000").isReadable("#ffff47", { level: "AAA" }); // false
colord("#808080").luminance(); // 0.22contrast() defaults the second color to #FFF, so always pass the real background. isReadable() defaults to WCAG AA and normal text; pass { size: "large" } for headings.
Convert to and from CSS color keywords (names plugin)css-color-names
import { colord, extend } from "colord";
import namesPlugin from "colord/plugins/names";
extend([namesPlugin]);
colord("tomato").toHex(); // "#ff6347"
colord("#00ffff").toName();// "cyan"
colord("#fe0000").toName();// undefined (not a CSS keyword)
colord("#fe0000").toName({ closest: true }); // "red"The names plugin is 1.45 KB of the bundle by itself, more than half the core, because it carries the full keyword table. Skip it if you only ever handle hex.
Mix two colors and build tints, shades and tones (mix plugin)mix-and-generate-shades
import { colord, extend } from "colord";
import mixPlugin from "colord/plugins/mix";
extend([mixPlugin]);
colord("#ffffff").mix("#000000").toHex(); // "#777777"
colord("#cd853f").mix("#eee8aa", 0.6).toHex();
colord("#ff0000").tints(3).map((c) => c.toHex()); // ['#ff0000','#ff9f80','#ffffff']
colord("#ff0000").shades(3).map((c) => c.toHex()); // ['#ff0000','#7a1b0b','#000000']Mixing happens in LAB, not RGB, which is why #ffffff mixed with #000000 lands on #777777 rather than #808080. The returned arrays include the original color as the first element.
Generate a harmony palette from one color (harmonies plugin)generate-harmonies
import { colord, extend } from "colord";
import harmoniesPlugin from "colord/plugins/harmonies";
extend([harmoniesPlugin]);
colord("#ff0000").harmonies("triadic").map((c) => c.toHex());
// ['#ff0000', '#00ff00', '#0000ff']
colord("#ff0000").harmonies("analogous").map((c) => c.toHex());
// ['#ff0080', '#ff0000', '#ff8000']Accepted types are analogous, complementary, double-split-complementary, rectangle, split-complementary, tetradic and triadic. These are hue rotations in HSL, so the results are a starting point for a designer, not a finished palette.
Compare two colors perceptually (lab and lch plugins)perceptual-difference
import { colord, extend } from "colord";
import labPlugin from "colord/plugins/lab";
import lchPlugin from "colord/plugins/lch";
extend([labPlugin, lchPlugin]);
colord("#3296fa").delta("#197dc8"); // 0.099
colord("#afafaf").delta("#b4b4b4"); // 0.014
colord("#213b0b").toLch(); // { l: 21.92, c: 30.45, h: 125.24, a: 1 }delta() is Delta E2000 normalized to 0 (identical) through 1 (opposite), so the usual "just noticeable difference" threshold of 1.0 Delta E is roughly 0.01 here. delta() lives in the lab plugin, not a11y.
Emit the shortest valid CSS string (minify plugin)minify-css-output
import { colord, extend } from "colord";
import minifyPlugin from "colord/plugins/minify";
import namesPlugin from "colord/plugins/names";
extend([minifyPlugin, namesPlugin]);
colord("black").minify(); // "#000"
colord("#112233").minify(); // "#123"
colord("rgba(170,170,170,0.4)").minify(); // "hsla(0,0%,67%,.4)"minify() will switch notation (hex to hsla) whenever that is shorter, so do not use it where the output format has to stay stable, such as values you diff in snapshot tests.
Use the exported TypeScript color typestyped-color-objects
import { colord, random, RgbColor, HslColor, AnyColor } from "colord";
const brand: HslColor = { h: 210, s: 90, l: 45 };
const accent: RgbColor = { r: 12, g: 90, b: 200 };
function toCss(input: AnyColor): string {
return colord(input).toHex();
}
random().toHex(); // e.g. "#01c8ec"Exported types include RgbColor, RgbaColor, HslColor, HslaColor, HsvColor, HsvaColor and AnyColor. They only describe object inputs; a plain string is still just string, so isValid() remains your only runtime check.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| culori | npm | You need OKLCH, OKLab, display-p3 or other CSS Color 4 spaces, with per-function imports so tree shaking actually works. |
| chroma-js | npm | Your job is color scales, gradients, and interpolated palettes rather than one-off conversions, and you can spend roughly 16 KB gzipped. |
| colorjs.io | npm | You want the spec-tracking implementation written by the CSS Color editors and correctness matters more than bundle size. |
| tinycolor2 | npm | You are on an older codebase that already uses it and do not need bundled TypeScript types or the smaller core. |