npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

unpalette

v0.0.1

Published

Color palette extraction, transformation, accessibility, quantization, and remapping for browsers and Node.js

Readme

unpalette

npm version npm license

Color palette extraction, transformation, accessibility, quantization, and remapping for browsers and Node.js — zero runtime dependencies, one algorithm everywhere.

Features

  • 🎨 Dominant color extraction — dominant-color clustering with largest-gap splitting, so a cut never lands inside a tight color cluster; colors come back sorted by pixel share with HEX output
  • 🪣 Quantization — reduce any RGBA8 image to its dominant palette, alpha preserved per pixel
  • 🖌 Remapping — apply a caller-provided palette (a fixed brand set, a terminal theme) with redmean nearest-color matching
  • ⚡ One algorithm, every runtime — pure TypeScript with zero runtime dependencies; the browser and Node produce identical palettes from identical pixels
  • 📦 Bounded work — large inputs are uniformly sampled down to a configurable pixel budget
  • 🌈 Palette generation — generatePalette derives complementary, analogous, triadic, split-complementary, and monochromatic palettes from one base color through classic color-wheel schemes
  • 🎭 Style variants — extractPaletteVariants reads one image into vivid, muted, dark, and light palettes by HSL attributes
  • 🧪 Color utilities — parse RGB, HSL, and OKLCH CSS colors, composite alpha, map out-of-gamut Oklch, interpolate in sRGB, linear sRGB, Oklab, or Oklch, generate scales and gradients, check WCAG contrast, and simulate color-vision deficiencies

Installation

# pnpm
pnpm add unpalette

# npm
npm install unpalette

# yarn
yarn add unpalette

# bun
bun add unpalette

Quick Start

import { applyPalette, extractPalette, quantize } from "unpalette";

const pixels = { data, width, height }; // RGBA8 — decode with unimage, canvas, or your own loader

// Dominant colors, sorted by pixel share.
const palette = extractPalette(pixels, { colors: 6 });
palette.colors.forEach(({ hex, ratio }) => console.log(hex, `${(ratio * 100).toFixed(1)}%`));

// Reduce the image onto its dominant palette (alpha preserved).
const reduced = quantize(pixels, { colors: 16 });

// Or remap onto a fixed brand palette.
const branded = applyPalette(pixels, [
  { r: 28, g: 28, b: 30 },
  { r: 255, g: 214, b: 10 },
  { r: 10, g: 132, b: 255 },
]);

extractPalette skips fully transparent pixels by default (alphaThreshold: 1), and samples large inputs down to maxPixels (default 65536) before analysis. Nearest-color matching uses the redmean distance — noticeably better than plain RGB Euclidean while staying cheap enough for per-pixel lookups.

Color utilities and scales

The same package includes the color math behind palette tools:

import {
  compositeColors,
  contrastRatio,
  formatColor,
  generateColorScale,
  generateGradient,
  lighten,
  mixColors,
  inGamut,
  readableTextColor,
  rgbToOklch,
  rotateHue,
  simulateColorVision,
} from "unpalette";

const brand = "#3366cc";
const scale = generateColorScale(brand, { steps: 9, space: "oklch" });
const accent = rotateHue(brand, 180);
const blended = mixColors(brand, "#ffcc00", { amount: 0.25, space: "oklch" });
const gradient = generateGradient([brand, accent], { steps: 7, space: "oklch" });
const rendered = compositeColors("rgba(255, 0, 0, 0.5)", "#ffffff");
const displayable = toGamut({ l: 0.8, c: 0.35, h: 20 });

console.log(formatColor(blended, "hex"));
console.log(rgbToOklch(brand));
console.log(inGamut({ l: 0.8, c: 0.35, h: 20 }), displayable);
console.log(readableTextColor(brand));
console.log(contrastRatio(brand, "#ffffff"));
console.log(simulateColorVision(brand, "deuteranopia", { severity: 0.75 }));

All functions are pure TypeScript and runtime-agnostic.

Generating palettes

Two paths produce palettes without decoding anything yourself:

import { extractPaletteVariants, generatePalette } from "unpalette";

// From one base color, through classic color-wheel schemes.
const complementary = generatePalette({ r: 255, g: 90, b: 0 }, "complementary");
const scale = generatePalette("#3366cc", "monochromatic");

// From one image: vivid, muted, dark, and light variants by HSL attributes.
const { vibrant, muted, dark, light } = extractPaletteVariants(pixels, {
  variantColors: 4,
});

Pairing with unimage

unpalette accepts raw RGBA8 pixels, so decoding lives wherever it belongs:

import { createImageManager } from "unimage";
import { sharpDriver } from "unimage/drivers/sharp";
import { extractPalette } from "unpalette";

const image = createImageManager({ driver: sharpDriver() });
const result = await image.transform(input, {
  operations: [{ type: "resize", width: 256 }],
  output: { format: "png", metadataPolicy: "strip" },
});

// Decode directly to RGBA8 pixels; no intermediate encoded image is needed.
const pixels = await image.decode(input);

const palette = extractPalette(pixels);

// Quantized pixels retain RGBA8 shape and can be passed back to unimage as raw input.
const reduced = quantize(pixels, { colors: 16 });
const rawImage = {
  data: reduced.data,
  width: reduced.width,
  height: reduced.height,
  layout: reduced.layout,
};

Related Projects

  • Graphics — the parent monorepo and ecosystem docs
  • unimage — decode image bytes into the RGBA8 pixels that palette APIs consume
  • Coolors — a reference for palette, tonal-scale, contrast, and color-vision workflows

License