# Color Thief: extracting dominant colors and palettes from images in the browser and Node.js

> Color Thief is an MIT-licensed TypeScript library that pulls dominant colors, palettes and semantic swatches from images, with the same API in the browser and Node.js. The interesting parts are the OKLCH quantization, region extraction and the deprecated worker option.

**lokesh/color-thief** — Grab the color palette from an image using just Javascript.  Works in the browser and in Node.

- Repository: https://github.com/lokesh/color-thief
- Website: https://lokeshdhakar.com/projects/color-thief/
- Stars: 13,644 · Forks: 1,312
- Language: TypeScript
- License: MIT
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/lokesh-color-thief

## What Color Thief solves, and who is actually reaching for it

The library answers one question: given a picture, what colors dominate it? The README frames the scope as "Extract dominant colors and palettes from images in the browser and Node.js." That is narrower than a general image-processing toolkit, and the narrowness is the point. You get a Color object back, not a pixel buffer you have to cluster yourself.

The audience is front-end and Node developers who need a palette for a UI decision: a background that matches album art, a placeholder tint derived from a hero image, a text color that stays readable on top of a generated swatch. The example files in the repository (examples/css, examples/img, examples/js, examples/video) point at the same use cases.

It is not a design-tool replacement. There is no color picker, no palette editing, no export to a design file. The output is data.

## How extraction works: sampling, quantization and the Color object

Color Thief reads pixels from a source, samples them, and clusters the samples into a small set of representative colors. The options table exposes the knobs. quality defaults to 10, described as a sampling rate where 1 means every pixel and 10 means every tenth. colorCount defaults to 10 and accepts 2 to 20. ignoreWhite defaults to true, so white pixels are skipped before clustering, which matters for product photos on white backgrounds.

The quantization space is selectable. colorSpace defaults to 'oklch', with 'rgb' as the alternative. The README describes the oklch path as "perceptually uniform palettes", meaning colors that look evenly spaced are treated as evenly spaced, which is not true in raw RGB. That is a real behavioural difference, not a formatting choice, and it is the default.

What comes back is a Color object rather than a tuple. It carries .rgb(), .hex(), .hsl(), .oklch(), .css(), .array(), .gamut, .textColor, .isDark, .isLight, .contrast, .population and .proportion. The .population and .proportion fields are the honest ones: they tell you how many pixels backed a swatch and what share of the total that was, so you can filter out colors that barely appear.

The .contrast property returns white, black and foreground WCAG ratios, and .textColor picks '#ffffff' or '#000000'. That is the library making a readability call for you, which is convenient and also a decision you may want to override.

## Installing Color Thief and pulling a palette from an image

The package is published as colorthief. The README gives the npm install as the primary path, and a CDN script tag as the alternative for a plain HTML page.

```bash
npm install colorthief
```

For a page with no build step, the README points at unpkg and the UMD bundle, which registers a global.

```html
<script src="https://unpkg.com/colorthief@3/dist/umd/color-thief.global.js"></script>
```

The quick start uses named exports. getColorSync takes an element and returns a Color; getPaletteSync takes the same source plus an options object. In a browser, the source can be an HTMLImageElement, HTMLCanvasElement, HTMLVideoElement, ImageData, ImageBitmap or OffscreenCanvas.

```js
import { getColorSync, getPaletteSync } from 'colorthief';

const img = document.querySelector('img');
const color = getColorSync(img);
console.log(color.hex());

const palette = getPaletteSync(img, { colorCount: 5 });
```

After that runs, color.hex() prints a string like '#e84393' and palette is an array of five Color objects. The README notes the sync functions are browser only; in Node you use the async forms, getColor, getPalette and getSwatches.

There is also a CLI, exposed through the bin entry as colorthief, which the README shows as colorthief photo.jpg with JSON, CSS and ANSI output. That is the fastest way to see what the library does to a file before wiring it into anything.

Region extraction is the feature worth trying on your second run. Coordinates are fractions of image size from the top-left, so the same numbers work on a thumbnail and the full-size original.

```js
const palette = await getPalette(img, {
    region: { x: 0, y: 0.66, width: 1, height: 0.34 },
});
```

The README states that a region running past the right or bottom edge is clamped to the image, while out-of-range or zero-sized values throw. That asymmetry is deliberate and worth knowing before you feed it computed coordinates.

## The worker option is deprecated, and that is the migration question

The release notes for v3.5.0 label the Web Worker path deprecated, and the options table is blunt about it: worker defaults to false, is "Deprecated", ignored as of v3, and removed in v4.

If your application currently relies on Color Thief spawning its own worker, the upgrade path is to own the worker yourself. The README's features list says the library is "Worker friendly" and runs inside your own Web Worker via ImageBitmap or OffscreenCanvas. So the supported route is to move the work into a worker you control and hand the library a transferable source. That is more code on your side and less magic on the library's side, which is a reasonable trade for a package with zero runtime dependencies, but it is a real migration cost and the README does not document a drop-in replacement for the old flag.

There is a second constraint: the sync functions are browser only. Code that shares a palette helper between a build step and a page cannot use getColorSync everywhere. You either branch, or you use the async functions in both places.

## Wide-gamut output, and where the P3 path stops

Display P3 support arrived in v3.4.0. The gamut option defaults to 'srgb' and accepts 'display-p3' or 'auto'. With 'auto', the README says the library reports P3 only when the image actually uses out-of-sRGB colors, otherwise it behaves exactly like sRGB.

The design choice here is conservative in a useful way. .rgb(), .array() and .hex() always return sRGB, gamut-mapped when the color is P3, so existing rgb(...) strings keep working. The wide-gamut values live in .css() and .oklch(), and .css() on a P3 color defaults to color(display-p3 ...). If you need the raw P3 components, the README points at .rgb('display-p3').

Two boundaries are stated plainly. The library falls back to sRGB where P3 canvas support is unavailable, so 'display-p3' is a request rather than a guarantee. And Node output is sRGB for now, so a server-side pipeline cannot produce P3 colors through this library today. If P3 fidelity on the server is a requirement, Color Thief is the wrong tool for that half of the job.

## getPaletteProgressive, observe and the async surface

getPaletteProgressive returns a 3-pass progressive palette as an async generator, described in the features list as "3-pass refinement for instant rough results". The shape matters: an async generator lets you render a rough palette immediately and replace it as passes complete, which is the right pattern for large images on slow connections. The README does not document the timing or the quality of each pass, so treat the early passes as approximate and do not build contrast decisions on the first yield.

observe() is the browser-only live path. It watches a video, canvas or img element and emits palette updates. The example passes throttle: 200 for milliseconds between updates, plus colorCount and an onChange callback, and returns a controller with a .stop() method. For images it uses a MutationObserver to detect src changes; for video and canvas it polls with requestAnimationFrame under the throttle. That difference is worth remembering, because a video element that is paused still gets polled on the animation frame schedule.

AbortSignal support is listed as a feature and appears as a signal option, which gives you a way to cancel in-flight extraction. The README does not describe what a cancelled call resolves or rejects with.

## Alternatives and how they differ

The most direct comparison is doing the clustering yourself on a canvas. You draw the image, call getImageData, quantize the pixels with your own k-means or median-cut loop, and map the results to hex. The difference in approach is where the decisions live. Color Thief ships the sampling rate, the quantization space, the white-pixel skip and the swatch classification as options and returns typed Color objects; a hand-rolled version gives you full control over the clustering algorithm and costs you the maintenance of it. If you need a specific clustering behaviour that Color Thief does not expose, the hand-rolled path is the one that will not fight you.

The second comparison is the semantic swatch model. Color Thief returns named buckets: Vibrant, Muted, DarkVibrant, DarkMuted, LightVibrant, LightMuted, reachable through getSwatches and getSwatchesSync. If you only need one dominant color, getColorSync is the smaller call and the swatch classification is overhead you are not using.

On the search results side, people look for a Python port and a React Native port. The package here is npm, TypeScript, browser and Node. The README does not describe either a Python or a React Native binding, so treat those as separate projects rather than as this one under another name.

## Licence, maintenance and upgrade cost

The repository is MIT licensed, and package.json declares "license": "MIT". MIT is permissive: you can use, modify and redistribute the code, including in commercial and closed-source products, provided the copyright notice and permission notice are preserved. That is the standard reading, not legal advice; check the LICENSE file in the repository and your own counsel for anything that matters.

Maintenance signals are mixed but readable. The last push to the default branch was on 2026-08-03, and v3.5.0 was tagged 2026-08-02, with v3.4.1 the same day and v3.4.0 on 2026-07-01. The repository is not archived. The release cadence over July and August 2026 was active, and the v3.4.0 release added a whole output gamut while v3.5.0 added region extraction and deprecated the worker path. A package that ships a deprecation alongside a feature is telling you the next major version has a breaking change in it.

Upgrade cost concentrates in two places. The worker option, ignored as of v3 and removed in v4. And the sync versus async split, which is stable but constrains how you share code between browser and Node. The package has zero runtime dependencies, so there is no transitive tree to audit, which keeps the dependency half of the upgrade cheap. The API half is where your time goes.

## Conclusion

Use Color Thief if you need client-side palette extraction from an image, canvas, video or ImageBitmap and want the same call shape in Node.js. Skip it if you need server-side native image decoding without a canvas implementation, or if you need the deprecated worker option to keep working past v4. Before adopting, run the region example against one of your own images and check that the returned palette is stable across the quality settings you plan to ship.

## FAQ

### What is Color Thief?

Color Thief is a TypeScript library that extracts dominant colors and palettes from images in the browser and Node.js. It is published on npm as colorthief, is MIT licensed, and has zero runtime dependencies.

### How can I find the dominant color in an image with Color Thief?

Import getColorSync in the browser and pass it an image, canvas, video, ImageData, ImageBitmap or OffscreenCanvas. It returns a Color object, and calling .hex() on it gives the dominant color as a string. In Node.js you use the async getColor instead, since the sync functions are browser only.

### What is a Color Thief alternative if I want to cluster pixels myself?

The alternative is drawing the image to a canvas and quantizing the pixels with your own k-means or median-cut routine. Color Thief exposes sampling rate, quantization space and white-pixel skipping as options and returns typed Color objects; a hand-rolled version gives you control over the clustering algorithm and makes you maintain it.

## Sources

- [License: MIT](https://github.com/lokesh/color-thief/blob/master/LICENSE)
- [lokesh/color-thief on GitHub](https://github.com/lokesh/color-thief)
- [Project website](https://lokeshdhakar.com/projects/color-thief/)
- [README](https://github.com/lokesh/color-thief/blob/master/README.md)
- [Releases](https://github.com/lokesh/color-thief/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/lokesh-color-thief
