color-thief-react-next
v1.0.0
Published
React hooks and components for colorthief v3 — dominant color, palettes, and semantic swatches. Works with Next.js Image.
Downloads
122
Maintainers
Readme
color-thief-react-next
React hooks and components for colorthief v3 — dominant color, palettes, and semantic swatches. Works seamlessly with the Next.js <Image> component.
Features
- Hooks + render-prop components —
useColor,usePalette,useSwatches - Two source modes — pass a React
ref(for Next.js<Image>) or a plain URL string - SSR safe —
colorthiefis dynamically imported insideuseEffect, never on the server - Abort on unmount — no state updates after unmount, no memory leaks
- Full TypeScript — rich
Colorobjects with.hex(),.hsl(),.oklch(),.isDark,.textColor, WCAG contrast ratios - Zero config — no hidden canvas setup, no CORS surprises if you follow the guide below
Install
npm install color-thief-react-next colorthiefPeer dependencies
npm install react@^18.0.0 react-dom@^18.0.0Usage
With Next.js <Image> (ref mode)
Pass a ref to the Next.js <Image> component and hand it to the hook. Next.js 13.4+ forwards the ref to the underlying <img> element.
'use client';
import Image from 'next/image';
import { useRef } from 'react';
import { useColor } from 'color-thief-react-next';
export function ProductCard({ src, alt }: { src: string; alt: string }) {
const imgRef = useRef<HTMLImageElement>(null);
const { data: color, loading, error } = useColor(imgRef);
return (
<div style={{ background: color?.hex() ?? '#f5f5f5', transition: 'background 0.3s' }}>
<Image
ref={imgRef}
src={src}
alt={alt}
width={400}
height={300}
crossOrigin="anonymous" // required for canvas pixel access
/>
{!loading && color && (
<p style={{ color: color.textColor }}>
{color.hex()} — {color.isDark ? 'dark' : 'light'}
</p>
)}
</div>
);
}Important: Add
crossOrigin="anonymous"to<Image>and ensure your image host sendsAccess-Control-Allow-Originheaders. Without this the canvas will be tainted and extraction will fail.
With a URL string
No ref needed — the hook creates a hidden <img> internally.
'use client';
import { useColor } from 'color-thief-react-next';
export function AvatarWithBg({ src }: { src: string }) {
const { data: color, loading } = useColor(src);
return (
<div style={{ background: loading ? '#eee' : color?.hex() }}>
<img src={src} alt="" crossOrigin="anonymous" />
</div>
);
}usePalette
'use client';
import { useRef } from 'react';
import Image from 'next/image';
import { usePalette } from 'color-thief-react-next';
export function PaletteStrip({ src }: { src: string }) {
const imgRef = useRef<HTMLImageElement>(null);
const { data: palette, loading } = usePalette(imgRef, { colorCount: 6 });
return (
<>
<Image ref={imgRef} src={src} alt="" width={400} height={300} crossOrigin="anonymous" />
{!loading && palette && (
<div style={{ display: 'flex', gap: 4 }}>
{palette.map((c, i) => (
<div key={i} style={{ width: 32, height: 32, background: c.hex() }} />
))}
</div>
)}
</>
);
}useSwatches
Returns semantic swatches: Vibrant, Muted, DarkVibrant, DarkMuted, LightVibrant, LightMuted.
'use client';
import { useSwatches } from 'color-thief-react-next';
export function SwatchDemo({ src }: { src: string }) {
const { data: swatches } = useSwatches(src);
return (
<div>
{swatches?.Vibrant && (
<div style={{ background: swatches.Vibrant.color.hex() }}>Vibrant</div>
)}
{swatches?.DarkMuted && (
<div style={{ background: swatches.DarkMuted.color.hex() }}>Dark Muted</div>
)}
</div>
);
}Render-prop components
All three hooks have equivalent render-prop components if you prefer that pattern.
import { ColorExtractor, PaletteExtractor, SwatchesExtractor } from 'color-thief-react-next';
// Dominant color
<ColorExtractor source="/photo.jpg">
{({ data, loading, error }) => (
<div style={{ background: data?.hex() ?? '#eee' }}>
{loading && <span>Extracting…</span>}
</div>
)}
</ColorExtractor>
// Palette
<PaletteExtractor source="/photo.jpg" options={{ colorCount: 5 }}>
{({ data }) =>
data?.map((c, i) => <div key={i} style={{ background: c.hex(), width: 24, height: 24 }} />)
}
</PaletteExtractor>
// Swatches
<SwatchesExtractor source="/photo.jpg">
{({ data }) => <div style={{ background: data?.Vibrant?.color.hex() }}>Vibrant</div>}
</SwatchesExtractor>API
Hooks
| Hook | Returns |
|---|---|
| useColor(source, options?) | ExtractionState<Color> |
| usePalette(source, options?) | ExtractionState<Color[]> |
| useSwatches(source, options?) | ExtractionState<Swatches> |
source is RefObject<HTMLImageElement> or a URL string.
Components
| Component | Props |
|---|---|
| <ColorExtractor> | source, options?, children(state) |
| <PaletteExtractor> | source, options?, children(state) |
| <SwatchesExtractor> | source, options?, children(state) |
Options
| Option | Default | Description |
|---|---|---|
| colorCount | 10 | Number of palette colors (2–20) — palette only |
| quality | 10 | Sampling rate (1 = every pixel, best quality) |
| colorSpace | 'oklch' | Quantization color space: 'rgb' or 'oklch' |
| ignoreWhite | true | Skip near-white pixels |
| worker | false | Offload to Web Worker (browser only) |
Color object (from colorthief v3)
| Property / Method | Description |
|---|---|
| .hex() | '#ff8000' |
| .rgb() | { r, g, b } |
| .hsl() | { h, s, l } |
| .oklch() | { l, c, h } |
| .css(format?) | 'rgb(…)', 'hsl(…)', 'oklch(…)' |
| .isDark / .isLight | Boolean |
| .textColor | '#ffffff' or '#000000' (WCAG-safe) |
| .contrast | { white, black, foreground } — WCAG ratios |
ExtractionState<T>
{
data: T | null;
loading: boolean;
error: Error | null;
}Swatches
type SwatchName = 'Vibrant' | 'Muted' | 'DarkVibrant' | 'DarkMuted' | 'LightVibrant' | 'LightMuted';
type Swatches = Partial<Record<SwatchName, { color: Color }>>;CORS note
colorthief uses a <canvas> internally to read pixel data. The browser blocks canvas reads on cross-origin images unless:
- The
<img>(or Next.js<Image>) hascrossOrigin="anonymous". - The image server responds with
Access-Control-Allow-Origin: *(or your origin).
If your images are on S3/CloudFront/CDN, add the CORS header on the bucket policy or CDN origin config.
License
MIT
