scalpel@labs: ~/glossary/color-palette-extraction.mdx5 sections

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.

extension: Scalpel Colorupdated: 2026-08-14read_time: 3 min
less color-palette-extraction.mdx

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:

  1. Capture: The tabs.captureVisibleTab API renders the visible viewport as a PNG bitmap (what you see on screen at that moment, nothing below the fold).

  2. Downscale: Reduce the bitmap to a smaller pixel budget (typically 10–20k pixels) to speed processing without losing major colours.

  3. 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.

  4. 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.

  5. 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.

Sources