Colour Palette Extraction - Lift a Palette From Any Page
Palette extraction captures the visible page as a bitmap and reduces its thousands of pixels to a short list of dominant colours, ranked by how much area each covers. It gives you a real site's working palette in one click.
Why it matters
Reading a colour from a screenshot by hand misses the point: you want the handful of colours a design actually leans on, weighted by how much they are used. Extraction does that by counting.
The result is practical: instead of eyeballing six colours from a site's header and missing the accent colour, you get an algorithm that looks at every visible pixel, finds the 8 or 16 most common ones, and ranks them by population. That is your real palette.
Two honest caveats: it only sees what is on screen, so scroll position changes the result, and near-white background pixels are skipped so the palette is not swamped by page background.
How it works
The pipeline has five steps:
-
Capture: The
tabs.captureVisibleTabAPI renders the visible viewport as a PNG bitmap (what you see on screen at that moment, nothing below the fold). -
Downscale: Reduce the bitmap to a smaller pixel budget (typically 10–20k pixels) to speed processing without losing major colours.
-
Quantise: Use median-cut quantisation to split the pixel space into boxes, with each box representing one dominant colour. The algorithm adapts to where colours actually cluster in the image, so a sunset photo keeps its oranges and reds instead of collapsing into grey.
-
Rank: Count how many pixels fall into each box and sort by population. The first colour in the result covers the most screen real estate.
-
Filter: Skip near-white pixels (to exclude page background) and near-black pixels (to exclude text shadows). Keep only colours in the middle range unless the near-white or near-black is genuinely significant (like a site with a white logo on a black background).
The extraction reads the visible viewport only, not the whole document. If you scroll halfway down a page and scan again, you get a different palette because the viewport has changed. That is intentional: it lets you extract the palette of the hero section separately from the footer.
What does not matter
Extraction is not colour science. If a page has 50 shades of blue, the algorithm picks the most common ones, but it does not know whether they are intentional brand colours or rendering artifacts. Use extraction as a starting point, not the final word. Clean up the palette after extraction: merge near-duplicates, drop accidental pixels, rename entries from numeric IDs to semantic names.
Code example
Simple extraction pipeline (pseudo-code):
const extractPalette = async (tabId, colorCount = 8) => {
// Capture the visible viewport as a PNG
const dataUrl = await chrome.tabs.captureVisibleTab(tabId, {
format: "png",
});
// Load the PNG as a bitmap
const response = await fetch(dataUrl);
const blob = await response.blob();
const bitmap = await createImageBitmap(blob);
// Draw to canvas and read pixel data
const canvas = document.createElement("canvas");
canvas.width = 200; // Downscale to 200x200 for speed
canvas.height = 200;
const ctx = canvas.getContext("2d");
ctx.drawImage(bitmap, 0, 0, 200, 200);
const imageData = ctx.getImageData(0, 0, 200, 200);
const pixels = imageData.data; // RGBA flat array
// Convert to RGB and filter near-white/near-black
const rgbPixels = [];
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const brightness = (r + g + b) / 3;
// Skip near-white (>240) and near-black (<15)
if (brightness > 15 && brightness < 240) {
rgbPixels.push([r, g, b]);
}
}
// Apply median-cut quantisation to find dominant colours
const dominantColors = medianCutQuantize(rgbPixels, colorCount);
// Return sorted by frequency
return dominantColors.sort((a, b) => b.frequency - a.frequency);
};
// Result: [
// { hex: "#38bdf8", rgb: [56, 189, 248], frequency: 0.25 },
// { hex: "#ffffff", rgb: [255, 255, 255], frequency: 0.18 },
// { hex: "#1e293b", rgb: [30, 41, 59], frequency: 0.12 },
// ...
// ]
Wrong – extracting all colours without quantisation:
// Listing every unique colour on a page is useless
// You get thousands of entries, most of them accidental
const getAllColorsNaive = (pixels) => {
const colors = new Set();
for (let i = 0; i < pixels.length; i += 4) {
const hex = rgbToHex(pixels[i], pixels[i + 1], pixels[i + 2]);
colors.add(hex);
}
return Array.from(colors);
// Result: 5000+ colours, 90% of them one-off pixels
};
How Scalpel Color shows it
The Palette tab has a "Scan visible page" button. Click it to capture and extract. The panel displays a slider to control how many colours the extraction finds (default 8, range 4–16). More colours give you fine detail; fewer give you just the major ones. The result shows each colour as a swatch with its hex code, the percentage of the viewport it covers, and a "Copy as" menu to grab it in any format.
If the palette includes colours you don't want (accidental pixels), click the X on that swatch to remove it. The remaining colours are still ranked by population. Export the final palette as CSS variables, Tailwind config, or JSON to feed into your design tokens.