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