Library / SDK
gka/chroma.js avatar
gka/chroma.js

chroma.js: a zero-dependency JavaScript library for color scales and conversions

JavaScript library for all kinds of color manipulations

10,599 stars551 forksJavaScriptNOASSERTION

At a glance

What is it?
Gregor Aisch's library keeps a small dependency-free surface for reading, converting and scaling colors, ships a light build alongside the full one, and has spent its recent releases on scale domains and reproducibility rather than new syntax.
Who is it for?
The library's shape has stayed narrow for years: a chainable color object, a scale function with selectable interpolation modes and domain strategies, and nothing that requires a build step of your own. If you need categorical palettes that pass through Lab instead of RGB, or quantile and log domains over unevenly distributed values, the parts that matter are in the scale function and you can read that code before depending on it.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 27 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What chroma.js actually covers

chroma.js is described in its own package metadata as a JavaScript library for color conversions, and the repository description widens that to all kinds of color manipulations. The scope in practice is narrower and more useful than either phrase suggests. It parses colors from strings and numbers, converts them between spaces including RGB, Lab and Lch, exposes arithmetic-style operations on a color object, and builds continuous and discrete scales from arrays of colors.

The distinguishing detail is the dependency count. The readme calls it a small-ish zero-dependency library and links a bundle size badge, which is a claim worth taking seriously for a package that ends up in visualization code and design tools. There is no color space conversion table to configure, no plugin registry, and no color management profile to load first. You pass a color in and you get a color out.

The author is Gregor Aisch, whose name appears in the package metadata as both author and sole maintainer, with vis4.net as the listed homepage alongside the documentation site at gka.github.io/chroma.js. Topics attached to the repository cluster around conversion and manipulation rather than any particular rendering target, which matches what the code does: nothing in the library paints to a canvas or touches the DOM.

Installing and importing

Installation is a single package name, and the readme shows the same line twice, once in a fenced block and once as an indented command for the node.js section:

code
npm install chroma-js

The package declares itself an ES module through the type field in its package metadata, and the documented import is a default import:

javascript
import chroma from "chroma-js";

The export map is more interesting than a single entry point. It resolves the bare specifier to index.js for import and to a CommonJS file under dist for require, and it also publishes a light variant under a subpath:

code
    ".": {
      "import": "./index.js",
      "require": "./dist/chroma.cjs"
    },
    "./light": {
      "import": "./index-light.js",
      "require": "./dist/chroma-light.cjs"
    },

That second entry is the answer to a question bundle analyzers ask constantly. The light build drops the color name tables, which are the bulk of the payload, so code that only needs numeric conversion and scales can skip them. The root of the repository carries matching files for each distribution shape, including UMD builds for script tags.

Development tooling is pinned to pnpm rather than npm, with the package manager field naming [email protected], and a pnpm lockfile sits at the repository root. That only matters if you intend to work on the library itself, but it does mean the documented install and the maintainer's own install path differ.

Chainable color operations

The core object is immutable and chainable. Each operation returns a new color rather than mutating the one you started with, which is what makes it safe to hold a base color and derive several variants from it. The readme opens with the shortest possible example:

javascript
chroma('#D4F880').darken().hex();  // #a1c550

Two things are worth noting in that line. Input parsing is forgiving, so a hex string with a leading hash is enough to get a working color object, and output formatting is explicit, because nothing is rendered until you ask for a string representation with a call like hex().

The same object model carries the rest of the API. Mixing two colors, blending them with a ratio, adjusting luminance, or comparing perceived brightness all go through the same chain, and each step can be serialized back out. Perceptual spaces matter here: the library can hold a color in Lab or Lch coordinates internally, so a luminance adjustment moves brightness without dragging saturation down the way an equivalent operation in HSL would.

There is also a random color entry point. The v3.1.4 release widened it to accept a random number generator as an argument, which is the kind of change that matters when you want a palette that reproduces between runs.

Scales, interpolation modes and domains

Scales are the reason most projects reach for this library. A scale takes colors and returns a function that maps a numeric input to an interpolated color:

javascript
scale = chroma.scale(['white', 'red']);
scale(0.5).hex(); // #FF7F7F

Interpolation happens in a chosen color space, and the default is a perceptual one rather than plain RGB. Switching modes is a single call, and the readme pairs the suggestion with a reason:

javascript
chroma.scale(['white', 'red']).mode('lab');

Interpolating a white-to-red ramp in RGB walks through muddy gray. Walking the same ramp in Lab keeps perceived lightness roughly even, which is the difference between a gradient that reads as one continuous transition and one that looks like it dips in the middle.

Scales also accept named palettes and non-uniform domains. ColorBrewer schemes plug in by name, and the domain argument supports quantile mapping so a handful of output colors line up with unevenly distributed input values:

javascript
chroma.scale('RdYlBu').domain(myValues, 7, 'quantiles');

For values spanning orders of magnitude there is a logarithmic mode, where evenly spaced output steps correspond to multiplicative rather than additive input changes:

javascript
chroma.scale(['lightyellow', 'navy']).domain([1, 100000], 7, 'log');

That combination of mode and domain is where the library earns its keep. A single linear scale across a domain like that collapses almost every useful value into the first color.

Repository layout and the build pipeline

The tree is organized around a source directory and a build output directory, with configuration files sitting at the root next to the entry points. Source modules are grouped by role rather than by feature, and the package export map exposes those groups directly, with separate subpaths for colors, generator, interpolator, io, ops and utils under src. That layout means you can import a single operation module instead of the whole library if you need to keep a build narrow, and the grouping tells you where a given operation lives.

The build is a Rollup configuration driven by three scripts declared in the package metadata:

code
    "build": "rollup -c && cross-env DEV=1 rollup -c ",
    "docs": "cd docs && make",
    "test": "vitest",

Each stage runs twice in the build script, once normally and once with a development flag set through cross-env, which is how the distribution gets both its regular and light output. Tests run under vitest, with a config file of the same name at the root, and there is a separate update mode for refreshing snapshots. Linting is split between prettier for formatting checks and eslint for the source, with the check list covering the entry points, src and the config files.

The docs are not generated by a JavaScript toolchain. The docs script changes into a docs directory and runs make, and a separate preview script runs make twice, once to build and once to preview. The readme also points at a static markdown version of the interactive documentation, which is useful when you want to read the API reference without a browser. One stale detail is worth flagging: the build instructions in the readme describe compiling coffee-script source files, while the repository tree shows JavaScript files under src, so those instructions describe an earlier era of the build.

Release history and maintenance reality

The last three published releases cluster in late 2025 and none of them adds API surface. v3.1.3 fixed a documentation link, improved the sidebar in the docs interface, and bumped dependencies. v3.1.4 fixed the discord link and allowed chroma.random to take a random number generator argument. v3.2.0, the newest, changed scale.domain to return all scaled positions instead of only the minimum and maximum, then shipped a follow-up fix to the scale.domain implementation in the same release.

That last change is the kind of quiet behavioral difference that can surface as a failing test rather than an error, which is worth knowing before you pin or upgrade. A contributor's pull request to expand what domain returns was accepted, and the maintainer adjusted the implementation before tagging.

The repository itself is not archived, carries 65 open issues, and its last push is dated September 2026, so the code is being touched. The readme preempts the more anxious question in its own FAQ section, where the maintainer explains that gaps between commits reflect a working library rather than an abandoned one, and points contributors to the project's discord channel. Licensing follows the same pattern of statements in prose: the readme describes releases under the BSD license, with versions before 0.4 under the GPL, while the API metadata carries no machine-readable license identifier, so check the LICENSE file in the repository if that distinction matters to your compliance process.

Editorial conclusion

The library's shape has stayed narrow for years: a chainable color object, a scale function with selectable interpolation modes and domain strategies, and nothing that requires a build step of your own. If you need categorical palettes that pass through Lab instead of RGB, or quantile and log domains over unevenly distributed values, the parts that matter are in the scale function and you can read that code before depending on it. Pin the version you test against, keep the full build rather than the light one unless you have measured the bundle, and remember that v3.2.0 changed what scale.domain reports, so a snapshot test written against 3.1.x may need updating.

Frequently asked questions

How do I build a color scale in chroma.js?

Pass an array of colors to chroma.scale, and it returns a function that maps a number to a color. For example, chroma.scale(['white', 'red']) produces a ramp, and calling the result with 0.5 gives the midpoint, which converts to #FF7F7F. Pick the interpolation space with mode, use a named scheme such as 'RdYlBu' for palettes from ColorBrewer, and pass a domain when your input values are unevenly distributed, for example chroma.scale('RdYlBu').domain(myValues, 7, 'quantiles').

Should I import chroma-js from the root or the light build?

The root entry point includes the full library, including the color name tables that make named colors work. The light build, published under the chroma-js/light subpath, drops those tables and is noticeably smaller. Code that only reads hex strings, converts numeric values and builds scales has no use for the name tables and can take the lighter file. The export map resolves each subpath for both import and require, so the choice is a specifier change rather than a bundler alias.

Why does my gradient look dull in the middle?

The usual cause is interpolating in RGB. Averaging channel values does not track perceived brightness, so a light color fading to a dark one passes through a grayish midpoint. Select a perceptual space for the scale with mode('lab'), which is also the library default for most scale operations. The readme calls out Lab and Lch interpolation specifically as producing better results than RGB for ramps like white to red.

What changed in chroma.js v3.2.0?

The behavior of scale.domain changed. Before v3.2.0 it reported only the minimum and maximum of the domain, and in v3.2.0 it returns all scaled positions instead. A separate fix to the scale.domain implementation landed in the same release, published in November 2025. If code or a snapshot test depends on the length of the array that domain returns, that expectation needs updating when you upgrade.

Official sources

  1. gka/chroma.js on GitHub
  2. Issues
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/gka-chroma-js.svg)](https://hysenlabs.com/projects/gka-chroma-js)