01
Loading the Library
Color Thief works in browsers and Node.js. Pick the method that fits your setup.
Install
npm install colorthief
ESM (bundlers & Node.js)
import { getColorSync, getPaletteSync } from 'colorthief';
CommonJS (Node.js)
const { getColor, getPalette } = require('colorthief');
Script tag (no build step)
<script src="https://unpkg.com/colorthief@3/dist/umd/color-thief.global.js"></script>
<script>
const color = ColorThief.getColorSync(img);
</script>
ES module in the browser (no bundler)
<script type="module">
import { getColorSync } from 'https://unpkg.com/colorthief@3/dist/index.js';
</script>
02
getColorSync()
The simplest way to use Color Thief. Extract the single dominant color from an image, synchronously.
const color = getColorSync(img);
color.hex(); // '#e84393'
color.textColor; // '#000000'
03
getPaletteSync()
Extract a multi-color palette. Each color in the palette is a full Color object.
const palette = getPaletteSync(img, { colorCount: 8 });
palette.forEach(c => console.log(c.hex()));
04
Color Object
Every extracted color is a rich object with format conversions, accessibility metadata, and contrast ratios.
const color = getColorSync(img);
color.rgb() // { r, g, b }
color.hex() // '#rrggbb'
color.hsl() // { h, s, l }
color.oklch() // { l, c, h }
color.css() // 'rgb(255, 128, 0)' — also 'hsl' and 'oklch'
color.array() // [r, g, b]
color.toString() // '#rrggbb' — works in template literals
color.textColor // '#ffffff' or '#000000'
color.isDark // true/false
color.isLight // true/false
color.contrast // { white, black, foreground }
color.population // raw pixel count
color.proportion // 0–1 share of total
05
getSwatchesSync()
Classify palette colors into six semantic roles: Vibrant, Muted, DarkVibrant, DarkMuted, LightVibrant, LightMuted. Each swatch includes text color recommendations.
const swatches = getSwatchesSync(img);
swatches.Vibrant?.color.hex(); // '#e84393'
swatches.DarkMuted?.titleTextColor.hex(); // '#ffffff'
06
OKLCH vs RGB Quantization
OKLCH quantization produces more perceptually uniform palettes. Colors that "feel" evenly spaced to the human eye.
// Default — quantize in sRGB
const rgb = getPaletteSync(img, { colorCount: 8 });
// Perceptual — quantize in OKLCH
const oklch = getPaletteSync(img, { colorCount: 8, colorSpace: 'oklch' });
07
Quality Settings
The quality option controls how many pixels are sampled. Lower values sample more pixels (slower, more accurate). Default is 10.
getPaletteSync(img, { quality: 1 }); // Every pixel
getPaletteSync(img, { quality: 10 }); // Every 10th pixel (default)
getPaletteSync(img, { quality: 50 }); // Every 50th pixel
08
Async API & Web Workers
The async API works on both browser and Node.js. With worker: true, quantization runs off the main thread.
// Async (works in browser and Node.js)
const palette = await getPalette(img, { colorCount: 6 });
// Offload to Web Worker (browser only)
const palette = await getPalette(img, { colorCount: 6, worker: true });
09
getPaletteProgressive()
Progressively extract a palette in 3 passes. Show a rough result instantly, then refine it. Useful for large images where full extraction takes time.
for await (const { palette, progress, done } of getPaletteProgressive(img)) {
updateUI(palette, progress);
// progress: 0.06 → 0.25 → 1.0
}
10
AbortController
Cancel in-flight extractions with a standard AbortSignal. Useful when the user navigates away before extraction completes.
const controller = new AbortController();
controller.abort(); // cancel immediately
try {
await getColor(img, { signal: controller.signal });
} catch (e) {
console.log(e.name); // 'AbortError'
}
11
createColor()
Build a Color object manually from RGB values. Useful for creating colors programmatically or working with known values.
const color = createColor(232, 67, 147, 1);
color.hex(); // '#e84393'
color.isDark; // false
color.textColor; // '#000000'
`${color}`; // '#e84393' — toString() returns hex
12
observe()
Reactively watch a source and get palette updates whenever it changes. Works with <video>, <canvas>, and <img> elements. For video, extraction runs on each animation frame (throttled) while playing, and on seek. For canvas, it polls via rAF. For images, it uses a MutationObserver to detect src changes.
const controller = observe(videoElement, {
throttle: 200, // ms between updates
colorCount: 5,
onChange(palette) {
const dominant = palette[0];
document.body.style.background = dominant.css();
},
});
controller.stop(); // clean up
================================================
FILE: package.json
================================================
{
"name": "colorthief",
"version": "3.3.1",
"type": "module",
"author": {
"name": "Lokesh Dhakar",
"email": "lokesh.dhakar@gmail.com",
"url": "http://lokeshdhakar.com/"
},
"description": "Extract dominant colors and palettes from images — TypeScript, OKLCH, semantic swatches, live video extraction.",
"keywords": [
"color",
"palette",
"sampling",
"image",
"picture",
"photo",
"canvas",
"oklch",
"swatch"
],
"homepage": "http://lokeshdhakar.com/projects/color-thief/",
"repository": {
"type": "git",
"url": "git+https://github.com/lokesh/color-thief.git"
},
"license": "MIT",
"bin": {
"colorthief": "dist/cli.js"
},
"files": [
"dist/",
"src/"
],
"exports": {
".": {
"browser": {
"import": {
"types": "./dist/types/index.d.ts",
"default": "./dist/index.browser.js"
},
"require": {
"types": "./dist/types/index.d.cts",
"default": "./dist/index.browser.cjs"
}
},
"import": {
"types": "./dist/types/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/types/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./internals": {
"browser": {
"import": {
"types": "./dist/types/internals.browser.d.ts",
"default": "./dist/internals.browser.js"
},
"require": {
"types": "./dist/types/internals.browser.d.cts",
"default": "./dist/internals.browser.cjs"
}
},
"import": {
"types": "./dist/types/internals.d.ts",
"default": "./dist/internals.js"
},
"require": {
"types": "./dist/types/internals.d.cts",
"default": "./dist/internals.cjs"
}
},
"./cli": {
"import": "./dist/cli.js"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/types/index.d.ts",
"scripts": {
"prepublishOnly": "npm run build",
"build": "tsup",
"watch": "tsup --watch",
"dev": "http-server",
"test": "mocha && cypress run --config video=false",
"test:node": "mocha",
"test:browser": "cypress run --headed --browser chrome",
"cypress": "cypress open",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/node": "^20.11.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"cypress": "^13.15.0",
"http-server": "^14.1.1",
"mocha": "^10.2.0",
"mustache": "^3.0.1",
"sharp": "^0.34.5",
"tsup": "^8.0.0",
"typescript": "^5.3.0"
},
"peerDependencies": {
"sharp": ">=0.33.0"
},
"peerDependenciesMeta": {
"sharp": {
"optional": true
}
}
}
================================================
FILE: src/api.ts
================================================
import type {
Color,
ExtractionOptions,
ImageSource,
PixelData,
PixelLoader,
ProgressiveResult,
Quantizer,
SwatchMap,
} from './types.js';
import { validateOptions, extractPalette } from './pipeline.js';
import { extractProgressive } from './progressive.js';
import { classifySwatches } from './swatches.js';
import { MmcqQuantizer } from './quantizers/mmcq.js';
import { resolveDefaultLoader } from './resolve-loader.js';
// ---------------------------------------------------------------------------
// Global configuration
// ---------------------------------------------------------------------------
let globalLoader: PixelLoader