colorthief vs node-vibrant
Extracting Dominant Colors and Palettes from Images in Web Applications
colorthiefnode-vibrantSimilar Packages:

Extracting Dominant Colors and Palettes from Images in Web Applications

colorthief and node-vibrant are JavaScript libraries designed to extract dominant colors, vibrant tones, and color palettes from images in the browser or Node.js environments. Both enable developers to analyze image content and derive representative colors for UI theming, dynamic styling, or visual enhancements. colorthief uses a quantization algorithm based on the MMCQ (Modified Median Cut Quantization) method to return the most prominent color or a limited palette. node-vibrant, inspired by Android’s Palette API, goes further by categorizing extracted colors into semantic swatches like Vibrant, Muted, DarkVibrant, and LightMuted, offering richer contextual options for design systems.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
colorthief013,5861.48 MB63 months agoMIT
node-vibrant02,43520.2 kB224 months agoMIT

colorthief vs node-vibrant: Choosing the Right Tool for Image-Based Color Extraction

Both colorthief and node-vibrant solve a common frontend challenge: pulling meaningful colors out of images to drive dynamic UI behavior — think auto-themed cards, smart background overlays, or adaptive icon coloring. But they approach the problem differently in architecture, output richness, and integration complexity. Let’s break down how they compare in real-world scenarios.

🎨 Core Philosophy: Simplicity vs Semantic Richness

colorthief treats color extraction as a statistical problem. It analyzes pixel data and returns the most frequent or visually dominant colors using a well-known quantization technique. The result? A clean list of RGB or hex values — nothing more, nothing less.

// colorthief: Get dominant color or palette
const colorThief = new ColorThief();
const img = document.querySelector('img');

img.onload = () => {
  const dominantColor = colorThief.getColor(img); // [R, G, B]
  const palette = colorThief.getPalette(img, 5);   // [[R,G,B], ...]
};

node-vibrant, by contrast, mimics Android’s Palette API. It doesn’t just find colors — it classifies them into six semantic categories: Vibrant, Muted, DarkVibrant, DarkMuted, LightVibrant, and LightMuted. Each swatch includes not only the color but also associated text colors (title and body) optimized for readability.

// node-vibrant: Extract categorized swatches
import Vibrant from 'node-vibrant';

Vibrant.from('image.jpg').getPalette((err, palette) => {
  if (err) return;
  console.log(palette.Vibrant?.getHex());        // e.g., "#FF5733"
  console.log(palette.Muted?.getBodyTextColor()); // readable text color
});

This semantic layer makes node-vibrant far more powerful for design systems that need context-aware styling — but it comes at the cost of added complexity.

⚙️ Image Handling and Environment Support

colorthief works directly with <img> elements, <canvas>, or image data URLs in the browser. In Node.js, it requires a canvas implementation like canvas (from the canvas npm package) to process images. It assumes you’ve already loaded the image and made it available for pixel reading.

// Node.js usage with canvas
import { createCanvas, loadImage } from 'canvas';
import ColorThief from 'colorthief';

const img = await loadImage('photo.jpg');
const canvas = createCanvas(img.width, img.height);
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);

const colorThief = new ColorThief();
const dominant = colorThief.getColor(canvas); // works on canvas element

node-vibrant abstracts image loading internally. You pass a URL, file path, or buffer, and it handles fetching and decoding. This simplifies usage but means you relinquish control over image preloading, CORS handling, or custom decoding logic. In the browser, it uses <img> under the hood; in Node.js, it relies on sharp or jpeg-js depending on configuration.

// node-vibrant handles image loading automatically
Vibrant.from('/assets/hero.jpg')
  .quality(10) // reduce resolution for faster processing
  .getPalette()
  .then(palette => { /* ... */ });

If you need fine-grained control over image lifecycle (e.g., lazy loading, retry logic, or custom caching), colorthief’s explicit model gives you more flexibility.

🧠 Algorithmic Approach and Performance

colorthief uses MMCQ (Modified Median Cut Quantization), which efficiently reduces millions of pixels to a small set of representative colors. It’s fast for small palettes (≤10 colors) but slows down as palette size increases because it processes all pixels without downsampling by default.

You can improve performance by drawing the image to a smaller canvas first:

const smallCanvas = document.createElement('canvas');
smallCanvas.width = 100;
smallCanvas.height = 100;
smallCanvas.getContext('2d').drawImage(img, 0, 0, 100, 100);

const color = colorThief.getColor(smallCanvas); // much faster

node-vibrant always downsamples images (default quality = 10, meaning 1/10 resolution) before analysis, which keeps runtime consistent even for large images. Its algorithm clusters colors and then selects candidates based on saturation, luminance, and population to assign semantic labels. This extra step adds overhead but ensures meaningful categorization.

For high-throughput scenarios (e.g., analyzing hundreds of thumbnails), colorthief with manual downsampling may edge out node-vibrant in raw speed. But for typical single-image use cases, both perform adequately.

🖌️ Output Structure and Developer Experience

colorthief returns plain arrays:

  • getColor()[r, g, b]
  • getPalette(n)[[r,g,b], [r,g,b], ...]

You’re responsible for converting to hex, HSL, or CSS strings if needed:

const [r, g, b] = colorThief.getColor(img);
const hex = `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`;

node-vibrant returns a structured Swatch object for each category, with helper methods:

const vibrant = palette.Vibrant;
if (vibrant) {
  console.log(vibrant.getHex());           // "#FF5733"
  console.log(vibrant.getPopulation());    // number of pixels
  console.log(vibrant.getTitleTextColor()); // contrast-safe text color
}

This eliminates guesswork around text legibility and provides immediate design-ready values — a big win for product teams focused on polish.

🔒 Error Handling and Edge Cases

colorthief throws exceptions if the image isn’t loaded, is cross-origin without proper CORS headers, or lacks pixel data. You must wrap calls in try/catch or ensure image readiness:

img.crossOrigin = 'anonymous';
img.onload = () => {
  try {
    const color = colorThief.getColor(img);
  } catch (e) {
    // handle tainted canvas or empty image
  }
};

node-vibrant uses callback-style or Promise-based error handling. It gracefully returns null for missing swatches (e.g., no vibrant color found) and surfaces image load errors clearly:

Vibrant.from('bad-url.jpg').getPalette((err, palette) => {
  if (err) {
    console.error('Image failed to load or parse');
    return;
  }
  // palette may have null entries, but no crash
});

This makes node-vibrant slightly more resilient in unpredictable environments like user-uploaded content.

🤝 Similarities: Shared Ground Between colorthief and node-vibrant

Despite their differences, both libraries share core capabilities and constraints:

1. 🖼️ Require Access to Raw Pixel Data

  • Both depend on the ability to read image pixels, which means CORS compliance is mandatory for external images.
  • In browsers, this requires crossOrigin="anonymous" on <img> tags and proper server headers.
<!-- Required for both libraries -->
<img src="https://example.com/photo.jpg" crossorigin="anonymous" />

2. 🧩 Browser and Node.js Support (with caveats)

  • Both work in modern browsers.
  • Both support Node.js, but require additional dependencies (canvas for colorthief, sharp or jpeg-js for node-vibrant).

3. ⏱️ Asynchronous Processing

  • Neither blocks the main thread during analysis (though colorthief is synchronous once pixel data is available).
  • For best UX, run extraction after image load and show a placeholder during processing.

4. 🎨 Focus on Visual Dominance, Not Color Theory

  • Both prioritize perceptually dominant colors over aesthetically balanced palettes.
  • Neither generates complementary or analogous schemes — they reflect what’s in the image, not what goes with it.

📊 Summary: Key Differences

Featurecolorthiefnode-vibrant
OutputRaw RGB arraysSemantic swatches with helper methods
Use Case FitSimple dominant color or small paletteContext-aware UI theming
Image LoadingManual (you provide ready image/canvas)Automatic (handles fetch and decode)
Text Color Suggestions❌ No✅ Yes (title/body with contrast safety)
Performance Control✅ Full (downsample yourself)⚙️ Limited (via .quality() setting)
Error ResilienceThrows on bad inputReturns nulls / uses callbacks

💡 The Bottom Line

  • Reach for colorthief when you need a no-frills, predictable way to grab the main color from an image — like tinting a play button to match album art or generating a fallback background. It’s lean, battle-tested, and gets out of your way.

  • Choose node-vibrant when your design system demands intelligent color roles — for example, using a vibrant accent for headlines and a muted tone for captions, all derived from the same hero image. The semantic structure saves hours of post-processing and accessibility tuning.

Both are mature, stable tools. Your choice hinges on whether you value simplicity or semantic richness in your color pipeline.

How to Choose: colorthief vs node-vibrant

  • colorthief:

    Choose colorthief if you need a lightweight, straightforward solution for extracting the dominant color or a small fixed-size palette from an image with minimal setup. It works reliably across browsers and Node.js (with canvas support), and its simple API is ideal for use cases like auto-generating background tints or accent colors where semantic color roles aren’t required.

  • node-vibrant:

    Choose node-vibrant if your application benefits from semantically labeled color swatches—such as distinct vibrant, muted, or dark variants—for richer UI theming. It’s especially useful when building adaptive interfaces that respond to image content with nuanced color choices, though it requires more careful handling of image loading and may involve larger bundle overhead due to its broader feature set.

README for colorthief

Color Thief

Extract dominant colors and palettes from images in the browser and Node.js.

npm version npm bundle size types

Install

npm install colorthief

Or load directly from a CDN:

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

Quick Start

import { getColorSync, getPaletteSync, getSwatches } from 'colorthief';

// Dominant color
const color = getColorSync(img);
color.hex();      // '#e84393'
color.css();      // 'rgb(232, 67, 147)'
color.isDark;     // false
color.textColor;  // '#000000'

// Palette
const palette = getPaletteSync(img, { colorCount: 6 });
palette.forEach(c => console.log(c.hex()));

// Semantic swatches (Vibrant, Muted, DarkVibrant, etc.)
const swatches = await getSwatches(img);
swatches.Vibrant?.color.hex();

Features

  • TypeScript — full type definitions included
  • Browser + Node.js — same API, both platforms
  • Sync & async — synchronous browser API, async for Node.js and Web Workers
  • Live extractionobserve() watches video, canvas, or img elements and emits palette updates reactively
  • Web Workers — offload quantization off the main thread with worker: true
  • Progressive extraction — 3-pass refinement for instant rough results
  • OKLCH quantization — perceptually uniform palettes via colorSpace: 'oklch'
  • Semantic swatches — Vibrant, Muted, DarkVibrant, DarkMuted, LightVibrant, LightMuted
  • Rich Color objects.hex(), .rgb(), .hsl(), .oklch(), .css(), contrast ratios, text color recommendations
  • WCAG contrastcolor.contrast.white, color.contrast.black, color.contrast.foreground
  • AbortSignal — cancel in-flight extractions
  • CLIcolorthief photo.jpg with JSON, CSS, and ANSI output
  • Zero runtime dependencies

API at a Glance

FunctionDescription
getColorSync(source, options?)Dominant color (sync, browser only)
getPaletteSync(source, options?)Color palette (sync, browser only)
getSwatchesSync(source, options?)Semantic swatches (sync, browser only)
getColor(source, options?)Dominant color (async, browser + Node.js)
getPalette(source, options?)Color palette (async, browser + Node.js)
getSwatches(source, options?)Semantic swatches (async, browser + Node.js)
getPaletteProgressive(source, options?)3-pass progressive palette (async generator)
observe(source, options)Watch a source and emit palette updates (browser only)
createColor(r, g, b, population)Build a Color object from RGB values

Options

OptionDefaultDescription
colorCount10Number of palette colors (2–20)
quality10Sampling rate (1 = every pixel, 10 = every 10th)
colorSpace'oklch'Quantization space: 'rgb' or 'oklch'
workerfalseOffload to Web Worker (browser only)
signalAbortSignal to cancel extraction
ignoreWhitetrueSkip white pixels

Color Object

Property / MethodReturns
.rgb(){ r, g, b }
.hex()'#ff8000'
.hsl(){ h, s, l }
.oklch(){ l, c, h }
.css(format?)'rgb(255, 128, 0)', 'hsl(…)', or 'oklch(…)'
.array()[r, g, b]
.toString()Hex string (works in template literals)
.textColor'#ffffff' or '#000000'
.isDark / .isLightBoolean
.contrast{ white, black, foreground } — WCAG ratios
.populationRaw pixel count
.proportion0–1 share of total

Browser

import { getColorSync, getPaletteSync } from 'colorthief';

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

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

Accepts HTMLImageElement, HTMLCanvasElement, HTMLVideoElement, ImageData, ImageBitmap, and OffscreenCanvas.

Live extraction with observe()

import { observe } from 'colorthief';

// Watch a video and update ambient lighting as it plays
const controller = observe(videoElement, {
    throttle: 200,    // ms between updates
    colorCount: 5,
    onChange(palette) {
        updateAmbientBackground(palette);
    },
});

// Stop when done
controller.stop();

Works with <video>, <canvas>, and <img> elements. For images, it uses a MutationObserver to detect src changes. For video and canvas, it polls using requestAnimationFrame with throttle.

Node.js

import { getColor, getPalette } from 'colorthief';

const color = await getColor('/path/to/image.jpg');
console.log(color.hex());

const palette = await getPalette(Buffer.from(data), { colorCount: 5 });

Accepts file paths and Buffers. Uses sharp for image decoding.

CLI

Quick start

npx colorthief-cli photo.jpg

The colorthief-cli package bundles everything needed (including sharp for image decoding), so it works immediately with no extra setup.

Commands

# Dominant color
colorthief-cli photo.jpg

# Color palette
colorthief-cli palette photo.jpg

# Semantic swatches
colorthief-cli swatches photo.jpg

Output formats

# Default: ANSI color swatches
colorthief-cli photo.jpg
# ▇▇ #e84393

# JSON with full color data
colorthief-cli photo.jpg --json

# CSS custom properties
colorthief-cli palette photo.jpg --css
# :root {
#     --color-1: #e84393;
#     --color-2: #6c5ce7;
# }

Options

colorthief-cli palette photo.jpg --count 5        # Number of colors (2-20)
colorthief-cli photo.jpg --quality 1              # Sampling quality (1=best)
colorthief-cli photo.jpg --color-space rgb        # Color space (rgb or oklch)

Stdin is supported — use - or pipe directly:

cat photo.jpg | colorthief-cli -

Multiple files are supported. Output is prefixed with filenames, and --json wraps results in an object keyed by filename.

Note: If you already have colorthief and sharp installed in a project, you can also use colorthief directly as the command name (without the -cli suffix).

Links

Contributing

npm run build          # Build all dist formats
npm run test           # Run all tests (Mocha + Cypress)
npm run test:node      # Node tests only
npm run test:browser   # Browser tests (requires npm run dev)
npm run dev            # Start local server on port 8080

Releasing

# 1. Make sure you're on master with a clean working tree
git status

# 2. Run the full test suite
npm run build
npm run test:node
npm run test:browser   # requires npm run dev in another terminal

# 3. Preview what will be published
npm pack --dry-run

# 4. Tag and publish
npm version <major|minor|patch>   # bumps version, creates git tag
npm publish                       # builds via prepublishOnly, then publishes
git push && git push --tags

License

MIT - Lokesh Dhakar