Bhinish Dhiman

How to pull a colour palette from a photo, and check it can carry text

Drop the photo into the image picker, choose Dominant for what the image is mostly made of or Distinct for colours that differ, and read the contrast figure beside each swatch. That figure is measured against white. Swatches below 4.5:1 cannot be body text on a white page; switching to Accessible moves each one along its own lightness axis, hue held, until it clears 4.5:1. Nothing is uploaded.

A photograph is a good place to find colours that belong together and a poor place to find a text colour. The reasons are mechanical: what an extractor does to the pixels, and what photographs are made of. This guide walks through the image picker, then the figures behind the contrast line it prints under every palette.

Extracting the palette

  1. Load the photo. Drop it on the panel that says “Drop an image here”, or press Browse image. Anything your browser can decode will work; if it cannot read the file, the tool asks for a PNG, JPEG, WebP or SVG. Replace and Remove appear over the image once it is in.
  2. Choose the extraction. Dominant, Distinct or Accessible, under “Extraction”. The sentence below the buttons says what the current mode does, and the M key cycles through them.
  3. Set how many colours. The − and + buttons beside “Colours” go from 2 to 10. It starts at 5.
  4. Read each row. Every swatch shows its hex code, its share of the image as a percentage, and its contrast ratio against white.
  5. Read the line under the list. It says how many swatches would fail as body text on white and, if any pair becomes hard to tell apart under simulated colour blindness, how many pairs.
  6. Switch to Accessible if the text will sit on white, then export. Export copies the palette as CSS, OKLCH, Tailwind v4, SCSS, JSON, Figma tokens or SVG swatches, and Save here keeps it on this device.

Two links sit under the list. Check colour vision opens the swatches in the colour blindness simulator, and Build a ramp takes the first swatch into the ramp and token generator. The tool keeps a downscaled copy of the image for the tab, so following either link and coming back does not lose it.

What happens to the pixels

k-means clustering is the method underneath. It sorts sampled pixels into a fixed number of groups by assigning each pixel to its nearest centre, moving each centre to the average of its pixels, and repeating until nothing moves. Each swatch you see is one of those centres. The pipeline, read from the tool’s source:

  • The image is scaled so its longer side is at most 320 pixels before anything is read. A photo smaller than that is left as it is.
  • At most 6,000 pixels are sampled, at an even stride. A 4,000 × 3,000 photo becomes 320 × 240 = 76,800 pixels, so every 13th is read (76,800 ÷ 6,000 = 12.8, rounded up), which gives 5,908.
  • Pixels with an alpha value below 128 out of 255 are skipped, so a logo on a transparent background does not come back as black.
  • The grouping runs in OKLab, a colour space built so that the numerical distance between two colours tracks how different they look. In sRGB, distances between dark colours are overstated and those between light colours understated, which is how an sRGB extractor returns several near-identical darks from a photo that plainly has several different ones.
  • It runs for up to 24 rounds, and the starting centres involve no randomness: the first is the pixel halfway through the sample, and each next one is the pixel farthest from the centres already chosen. The same photo gives the same palette every time.

Dominant, Distinct and Accessible, compared

The three modes answer different questions, and the percentage beside each swatch means something slightly different in each.

Mode Groups computed What you get The percentage
Dominant The number you set Every group, largest first Share of the sampled pixels in that group
Distinct Twice the number plus two, at most 16 Groups taken largest first, skipping any within 0.1 of lightness and 0.05 of chroma of one already kept, then cut to the number you set Share within the larger grouping, so the column need not add up to 100
Accessible The number you set Dominant’s groups, with each one under 4.5:1 on white moved to the nearest lightness that passes As Dominant

Distinct’s duplicate test looks only at lightness and chroma, not hue, so two accents of different hue at similar lightness and chroma count as one. When a photo’s accents differ mainly in hue, Dominant at a higher count can be the better way to surface them.

Accessible searches lightness in steps of 0.01 and takes the passing value closest to the original, keeping the hue and, where the screen can show it, the chroma.

Why photo colours fail as text on white

Three things stack up against a photograph.

The centres are averages. A swatch is the mean of every pixel in its group, so the darkest swatch is lighter than the darkest pixels inside it: shadow gets averaged with near-shadow. The extremes a text colour needs are the first thing averaging removes.

Body text on white needs a dark colour, and most of a photograph is mid-tone. For 4.5:1 on white the WCAG formula caps relative luminance at 1.05 ÷ 4.5 − 0.05, about 0.183. For a neutral colour, OKLCH lightness is the cube root of relative luminance, so that is a lightness of about ∛0.183 ≈ 0.568. The worked examples in the site’s colour tool notes land in the same place: a blue at lightness 0.567 measures 4.50:1, and an amber at 0.572 measures 4.51:1.

The figure on each swatch is against white only. A sunset’s amber, #e8b04b, measures 1.95:1 on white at a lightness of 0.791. Accessible returns #a16c00 at 4.51:1, holding hue 79° and bringing lightness down to 0.572, a drop of 0.791 − 0.572 = 0.219. It is a visibly darker amber that still belongs to the photograph.

A dark page reverses the problem. A colour’s ratio on black is 21 divided by its ratio on white, so the original amber manages 21 ÷ 1.95, over 10:1, on black. The Accessible version manages 21 ÷ 4.51, just under 4.7:1, barely over the line. For text on a dark background, stay in Dominant and work out each swatch’s figure on black that way.

Against a typical extractor

This tool A typical online extractor
Grouping space OKLab sRGB
Modes Dominant, Distinct or Accessible, named One, unnamed
Usability check Contrast on white for every swatch None
A failing swatch Lightness search at the same hue Not offered
The image Decoded in the tab Posted to a server

Other ways to do it

There is no built-in method on macOS, Windows or iPhone for extracting a palette from a photo, and no command-line route among those these guides vouch for. If you already have colours from somewhere else, the contrast checker will tell you which can carry text and find the nearest version of each that can.

Checking that the photo stays put

The drop zone says the file is read in your browser, and you can confirm it. Open DevTools, choose the Network panel, then drop a photo: no request carrying the image goes out. Disconnecting from the network before you drop it works too, and the palette still appears.

Open the image picker with the photo you have in mind and read the line under the swatches before you fall for them. If the colour you want fails on white, How to fix colour contrast without changing your brand colour explains why moving lightness, not hue, keeps it recognisable.