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-forgeNo 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 jsonToken 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 rotationBy 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.