Documentation

palette-forge is the engine behind this site, published as a zero-dependency npm package. It runs in the browser, in Node, in a Worker, and from the command line.

Start here. The plain version

You have a picture. It has colours in it. You want those colours as code you can use in your own site or app. That is the entire tool.

The fastest way: go to the homepage and drag a picture onto it. The colours appear. Click one to copy it. You’re done. You can stop reading here.

What the four sections mean

The colours. Each rectangle is one colour found in your picture, with its code (like #4CC9F0) and how much of the picture it covers.

The labels. The tool guesses what each colour is for, primary is your main brand colour, ink is for text, paper is for backgrounds. It’s a guess. Use the dropdown under any colour to correct it.

The contrast matrix. Some colour pairs are hard to read, light grey text on white, for instance. This checks every pair and says PASS or FAIL. You don’t need to understand the numbers. Look for PASS.

The generated tokens. The actual code, ready to copy. The tabs are different flavours of the same thing, pick tokens.css if you’re unsure, or shadcn/ui if you use shadcn (it does the most work for you).

If something looks wrong

Only got white and grey? Your picture is probably a screenshot, and screenshots are mostly background. Tick Ignore background greys.

Too many similar colours? Turn the Colours slider down. You’re asking for more colours than the picture really has.

It missed a colour you can see? Turn the slider up, or press Try a different split.

Install

npm install palette-forge
# or: pnpm add palette-forge · yarn add palette-forge · bun add palette-forge

No runtime dependencies in the browser path. PNG and JPEG decoding in Node is bundled; sharp is an optional peer dependency that unlocks WebP, AVIF, TIFF, GIF and HEIC.

In the browser

extractPaletteFromImage accepts a File, Blob, <img>, ImageBitmap, canvas or URL.

import { extractPaletteFromImage, toCSS } from "palette-forge";

const palette = await extractPaletteFromImage(file, {
  colors: 6,
  downweightNeutrals: true, // demote greys so brand colours surface
});

palette.swatches[0].hex;        // "#4cc9f0"
palette.swatches[0].role;       // "primary"
palette.swatches[0].share;      // 0.34  (34% coverage)
palette.byRole.primary?.[0];    // the brand colour specifically

console.log(toCSS(palette));

Extraction is deterministic: the same image and options always produce the same palette, so results can be cached, diffed and asserted against in tests.

React hooks

palette-forge/react is headless, hooks and state, no markup and no styles.

"use client";
import { usePalette, useDropzone } from "palette-forge/react";

export function Forge() {
  const { palette, load, preview, status } = usePalette({ colors: 6 });
  const { rootProps, inputProps, isOver } = useDropzone({ onFile: load });

  return (
    <div {...rootProps} data-over={isOver}>
      <input {...inputProps} />
      {status === "loading" && <Spinner />}
      {palette?.swatches.map((s) => (
        <span key={s.hex} style={{ background: s.hex, color: s.on }}>
          {s.hex}
        </span>
      ))}
    </div>
  );
}

The decoded pixels are cached separately from the palette, so changing colors re-clusters in a millisecond or two without re-decoding the image, which is what makes a live slider feel instant.

In Node

import { extractPaletteFromFile } from "palette-forge/node";
import { toShadcn } from "palette-forge";
import { writeFile } from "node:fs/promises";

const palette = await extractPaletteFromFile("./brand/logo.png", { colors: 6 });
await writeFile("app/globals.css", toShadcn(palette));

Also available: extractPaletteFromBuffer, extractPaletteFromUrl and decodeImage.

CLI

# Look at a palette in the terminal, in true colour
npx palette-forge logo.png

# Generate a shadcn/ui theme
npx palette-forge logo.png --format shadcn --out app/globals.css

# Tailwind v4 theme with full 50 to 950 ramps, straight from a URL
npx palette-forge https://example.com/hero.jpg -f tailwind -o theme.css

# Audit a screenshot's accessibility
npx palette-forge screenshot.png --colors 8 --neutrals --contrast

# Pipe it
cat logo.png | npx palette-forge - -f json

Token formats

Nine emitters, all pure string builders: css, tailwind, scss, ts, js, json, dtcg, shadcn and svg.

import { emit, toTailwind, toDTCG } from "palette-forge";

emit(palette, "tailwind", { scales: true });  // @theme block, oklch, 50 to 950
emit(palette, "shadcn");                       // :root + .dark, AA-repaired
emit(palette, "dtcg");                         // W3C Design Tokens JSON
emit(palette, "css", { prefix: "brand" });     // --brand-primary: …

The shadcn emitter is the shortcut worth knowing: it derives a full semantic theme, background, card, popover, muted, border, ring, destructive, for light and dark, and contrast-repairs every text/surface pair to AA before emitting.

Contrast & repair

import { contrast, evaluateContrast, contrastMatrix, ensureContrast } from "palette-forge";

contrast("#767676", "#ffffff");          // 4.54
evaluateContrast("#767676", "#ffffff");  // { ratio, aaNormal, aaLarge, level: "AA", … }
contrastMatrix(hexes, { limit: 20 });    // every pairing, ranked

// Nudge a colour until it passes, holding its hue
ensureContrast("#4cc9f0", "#ffffff");                 // → "#0081a1" (4.51:1)
ensureContrast("#4cc9f0", "#ffffff", { target: 7 });  // → "#00617a" (7.02:1)

Tonal scales

Ramps are generated in OKLCH and gamut-mapped by reducing chroma, so hue holds across the whole ramp instead of skewing at the ends.

import { scale, neutralScale, harmony } from "palette-forge";

scale("#4cc9f0");                          // { 50: "#e8f9ff", …, 300: "#4cc9f0", …, 950: "#002935" }
scale("#4cc9f0", { saturation: 0.5 });     // muted, editorial
neutralScale("#4cc9f0");                   // greys carrying a trace of brand hue
harmony.complementary("#4cc9f0");          // OKLCH hue rotation

By default the source colour is anchored: it appears verbatim at its nearest stop, so the brand colour is genuinely in the ramp rather than approximated.

HTTP API

This site exposes the Node path at POST /api/extract for consumers without a canvas, CI jobs, bots, plugins.

curl -X POST https://palette-forge-web.vercel.app/api/extract \
  -F image=@logo.png -F colors=6

curl -X POST https://palette-forge-web.vercel.app/api/extract \
  -H 'content-type: application/json' \
  -d '{"url":"https://example.com/logo.png","format":"shadcn"}'

The browser app never calls it, extraction there is entirely local, and no image ever leaves the machine.

Words explained

Every technical term used on this page, in plain English.

hex code
A colour written as #4CC9F0. Just a way of naming a colour that computers understand.
palette
A set of colours that go together.
swatch
One single colour within a palette.
design token
A colour saved under a name (like `primary`) so you can reuse it everywhere and change it in one place.
contrast
How different two colours are in brightness. High contrast is easy to read; low contrast isn't.
WCAG
The international standard for what counts as readable. AA is the level almost everyone targets. It needs a contrast ratio of 4.5 for normal text.
ramp / scale
Light-to-dark versions of one colour, numbered 50 (lightest) to 950 (darkest). You need them for hover states, borders and backgrounds.
k-means
The method used to group similar colours together. A well-known algorithm from the 1950s. Not AI, no API key. It runs in about 3 milliseconds.
OKLab
A way of describing colours where equal numbers mean equal visible difference. Grouping colours in OKLab instead of RGB is why this tool doesn't hand back three near-identical blues.
medoid
The real colour from a group that best represents it, as opposed to the group's average, which might be a colour that was never in your picture.
deterministic
Same picture in, same colours out, every time. Means you can save the result and trust it won't change.
gamut
The set of colours a screen can actually display. Some colours exist in maths but not on your monitor.

The full glossary covers everything else.