Repository navigation
Releases: lokesh/color-thief
Release list
v3.5.0 — Region extraction; Web Worker path deprecated
Region extraction 🖼️
Sample colors from part of an image instead of the whole thing — the most-requested evergreen feature, open since 2021 (#176, with earlier attempts in #44 and #90). Thanks @runshotgun for the original ask.
New region option
Coordinates are fractions of the image size (0–1) from the top-left, so a region is resolution-independent — the same values work on a thumbnail and the full-size original.
// Colors from the bottom third — e.g. for a gradient overlapping the image
const palette = await getPalette(img, {
region: { x: 0, y: 0.66, width: 1, height: 0.34 },
});
// Center crop
const color = await getColor(img, {
region: { x: 0.25, y: 0.25, width: 0.5, height: 0.5 },
});Works everywhere an option object does: getColor, getPalette, getSwatches, getPaletteProgressive, the *Sync functions, observe(), and the CLI — in both browser and Node.
colorthief image.jpg --region 0,0.66,1,0.34What changed
- Cropping happens on the decoded pixel buffer right after loading, before sampling — so every entry point gets region support, including custom loaders and quantizers supplied via
configure(). - Regions are validated up front, so a malformed rect throws before the image is decoded rather than after.
- A region running past the right or bottom edge is clamped to the image; out-of-range or zero-sized values throw.
- Proportions are relative to the region, not the whole image.
⚠️ The worker option is now a no-op
worker: true is still accepted but ignored, and logs a one-time deprecation warning. The isWorkerSupported, extractInWorker, and terminateWorker exports from colorthief/internals are now no-op shims. Nothing breaks in v3 — all of it is removed in v4.
It cost more than it saved. Only quantization ran off-thread. Decoding, pixel sampling, and the structured clone of the pixel array all stayed on the main thread — and serializing Array<[r, g, b]> (one small array per sampled pixel) ran several times longer than the quantization it avoided:
| Image | Quantize (what the worker saved) | Structured clone |
|---|---|---|
| 0.3 MP, quality 10 | 0.7 ms | 2.9 ms |
| 2 MP, quality 10 | 2.5 ms | 19.9 ms |
| 12 MP, quality 10 | 12.9 ms | 199 ms |
The gap widens as images get larger — the opposite of how the feature was meant to scale.
It was also returning different colors. The worker carried a hand-inlined copy of MMCQ that quantized in RGB while the main path defaults to OKLCH, and it skipped the few-color short-circuit and filter relaxation. Two calls differing only by worker: true disagreed. Routing the flag through the normal pipeline makes them agree.
Getting extraction off the main thread
Run Color Thief inside your own worker and hand it an ImageBitmap. Bitmaps are transferable, so pixels move without being copied and the whole pipeline — decode, sampling, quantization — runs off-thread:
// main.js
const bitmap = await createImageBitmap(await (await fetch(url)).blob());
const worker = new Worker('./palette-worker.js', { type: 'module' });
worker.postMessage({ bitmap }, [bitmap]); // transferred, not cloned
worker.onmessage = (e) => render(e.data.palette);// palette-worker.js
import { getPalette } from 'colorthief';
self.onmessage = async ({ data }) => {
const palette = await getPalette(data.bitmap, { colorCount: 5 });
// Color objects don't survive structured clone — send plain data
self.postMessage({ palette: palette.map((c) => c.hex()) });
};Smaller bundles
Deleting the inlined quantizer shrank every build:
| Bundle | Before | After | |
|---|---|---|---|
dist/index.js |
55.6 kB | 46.2 kB | −17% |
dist/internals.js |
45.0 kB | 39.4 kB | −13% |
dist/umd/color-thief.global.js |
30.0 kB | 22.3 kB | −25% |
Notes
- Backward compatible — no breaking changes. The only behavior change is that
worker: truenow returns the same palette as the default path instead of a different one. - The deprecation warning fires once per page load, not once per call.
Full changelog: v3.4.1...v3.5.0
3.4.1
Patch release fixing a bundler warning reported in #283.
Fixed
WasmQuantizer fell back to importing ../../dist/wasm/color_thief_wasm.js when constructed with no arguments, but that file has never been included in the published package. Bundlers resolving colorthief/internals — for example to use oklchToRgb — reported it as an unresolvable module. Applications built and ran correctly (the dead code was tree-shaken away), but every build printed a warning.
The wasm-bindgen glue module is now supplied by the caller:
import { WasmQuantizer } from 'colorthief/internals';
const q = new WasmQuantizer(await import('./pkg/color_thief_wasm.js'));
await q.init();init() with no module now throws a message pointing at the wasm-pack build step. Neither of the old code paths could succeed from an npm install, so nothing that previously worked has changed.
Thanks to @magic-akari for the report and diagnosis.
Full Changelog: v3.4.0...v3.4.1
v3.4.0 — Wide-gamut (Display P3) support
Wide-gamut (Display P3) support 🎨
Color Thief can now read and report wide-gamut colors, fixing incorrect results on P3-tagged images where out-of-sRGB colors were collapsed down to sRGB (thanks @LeaVerou — #266).
New gamut option
Opt in per call (or via configure): 'srgb' (default), 'display-p3', or 'auto'.
const palette = await getPalette(img, { gamut: 'display-p3' });
palette[0].css(); // 'color(display-p3 …)'
palette[0].gamut; // 'display-p3'
// 'auto' upgrades to P3 only when the image actually uses out-of-sRGB colors
const auto = await getPalette(img, { gamut: 'auto' });What changed
- Browser loaders read the image through a P3 canvas (
getContext('2d', { colorSpace: 'display-p3' })) with feature detection and automatic sRGB fallback. - Gamut-aware OKLCH quantization preserves the extra saturation instead of clamping it away.
- Color objects now carry
.gamut..css()emitscolor(display-p3 …)and.oklch()reports the true wider chroma, while.rgb()/.array()/.hex()stay sRGB (gamut-mapped) so existing consumers keep working. Use.rgb('display-p3')for raw P3 components. - Threaded through the async, sync, worker, and progressive paths.
Notes
- Backward compatible — default behavior is unchanged sRGB.
- Node output is sRGB for now; P3 there is tracked as a follow-up.
Full changelog: v3.3.2...v3.4.0
v3.3.2
Performance & accuracy improvements
- Better results for images with few colors — improved performance and accuracy of extraction when an image contains only a handful of distinct colors (#281).
- Docs: consolidated planning docs into
ROADMAP.md, added roadmap items, and moved transparency detection to intentionally-left-out.
Full changelog: v3.3.1...v3.3.2
v3.3.1
Packaging and docs fixes for the 3.3.0 CLI release.
- Add a
colorthief/cliexport (./cli) for programmatic access to the CLI entry. - Apply
npm pkg fixcorrections: normalize thebinpath and repository URL for a clean publish. - Update README CLI references to
colorthief-cli.
v3.3.0
v3.2.0
Browser bundlers no longer warn about sharp - Bundlers like webpack (Angular), Vite, and Rollup previously emitted a "Can't resolve 'sharp'" warning when using colorthief in browser-only projects. The library now ships separate browser builds that contain no references to sharp or Node.js-specific code. Bundlers automatically pick the right build via the "browser" condition in package.json exports. No changes needed on your end. The API is identical. (#279)
v3.1.0
v3.0.0
Full TypeScript rewrite with a unified browser + Node.js API.
Highlights:
- Rich Color objects with .hex(), .rgb(), .hsl(), .oklch(), .css()
- OKLCH perceptually uniform quantization
- Semantic swatches (Vibrant, Muted, DarkVibrant, DarkMuted, LightVibrant, LightMuted)
- Live extraction with observe() for video, canvas, and img elements
- Web Worker offloading
- Progressive 3-pass extraction
- WCAG contrast ratios and text color recommendations
- AbortSignal support
- Zero runtime dependencies
v.2.7.0
New features:
- Configurable pixel filtering — New options: ignoreWhite, whiteThreshold, alphaThreshold, and minSaturation let callers control which pixels are
included in color extraction - Options object API — getColor and getPalette now accept an options object (e.g., { colorCount: 5, quality: 10, ignoreWhite: false }) in addition to the
existing positional arguments - New browser input types — getPalette/getColor now accept HTMLCanvasElement, ImageData, and ImageBitmap in addition to HTMLImageElement
- TypeScript type definitions — Added .d.ts files for both browser (color-thief.d.ts) and Node (color-thief-node.d.ts)
Bug fixes & robustness:
- Fallback color for edge cases — New computeFallbackColor() averages all pixels when the quantizer can't produce a palette (e.g., solid-color or
all-white images), preventing null returns - Progressive filter relaxation — If filtering removes all pixels, filters are progressively relaxed (first allowing white, then transparent) before
falling back - Fixed variable scope leak — i in getImageData changed from implicit global to let
- Input validation — Browser version now throws descriptive errors for unloaded images, missing sources, and CORS-tainted canvases
- Null-safe palette access — All call sites now check palette for null before accessing palette[0]
Chores & tooling:
- Removed file-type dependency from Node version (unused)
- Removed dist/ from git tracking (kept in npm package via .npmignore)
- Added GitHub Actions CI workflow
- Replaced CanvasImage class with a simpler getPixelData() function
- Updated README to reflect current project setup
- Version bump to 2.7.0 across package.json and source files
