How to check a colour palette for readable text: contrast ratios, worked through real palettes

Short answer: for every colour you put text on, measure the contrast ratio between the text colour and the background. Body text needs at least 4.5:1, large text (24 px, or about 18.5 px bold) needs 3:1, and the edges of buttons, fields and the parts of a chart you need to see also need 3:1. Do not round: 4.49:1 fails. A palette is not "accessible" or "not accessible" on its own. Pairs are. So list the pairs you actually use and check each one.

Below is what the number means, how to work it out yourself in a few lines of code, and five checks on palettes from the Sundry Mill library, including one where our own page gave the wrong advice until this week.

What a contrast ratio is

WCAG 2 defines contrast as (L1 + 0.05) / (L2 + 0.05), where L1 is the relative luminance of the lighter colour and L2 of the darker one. Relative luminance is how bright a colour looks to the eye on a scale from 0 (black) to 1 (white). Green counts for most of it, red for some, and blue for very little, which is why a bright blue can be surprisingly dark as a background and a yellow surprisingly hard to read on white.

The ratio runs from 1:1 (the same colour twice) to 21:1 (black on white). It does not care which colour is the text and which is the background, so dark text on a light card and light text on a dark card measure the same.

One detail if you write your own: the luminance formula has a threshold constant that was 0.03928 before May 2021 and is now 0.04045. WCAG's own note says the change "has no practical effect on the calculations". Our library still uses the older constant, and every figure in this post comes from it.

The numbers to remember

From the WCAG 2.2 success criteria:

  • 4.5:1 for normal text (1.4.3, level AA). This is the one that matters most.
  • 3:1 for large text. Large means at least 18 point, or 14 point bold. In CSS pixels that is about 24 px, or about 18.5 px bold.
  • 3:1 for the parts of controls and graphics you need to see (1.4.11, Non-text Contrast). The outline of a text field, the track of an off switch, a focus ring, the bars of a chart that carries information.
  • 7:1 for normal text and 4.5:1 for large text if you want the stricter AAA level (1.4.6).

The understanding document for 1.4.3 is blunt about rounding: computed values "should not be rounded (e.g., 4.499:1 would not meet the 4.5:1 threshold)". Logos and text in inactive controls have no contrast requirement, and neither does incidental text, like a street sign in a photo.

Check it yourself in ten lines

You do not need a tool for this. Here is the function the library uses to fill in the "Text that reads" list on every palette page. It takes two hex codes and returns the ratio.

function rgb(hex) {
  const n = parseInt(hex.slice(1), 16);
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
function lum(hex) {
  const [r, g, b] = rgb(hex).map((v) => {
    const c = v / 255;
    return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function contrast(a, b) {
  const [x, y] = [lum(a), lum(b)].sort((p, q) => q - p);
  return (x + 0.05) / (y + 0.05);
}

contrast('#351E28', '#D7EFFF'); // 12.93

Paste it into your browser's console, call it with your pairs, and keep the third decimal when a result is close to a threshold.

Check 1: a trend palette with one dark colour

The 2026 Pinterest Palette has five colours: Cool Blue #D7EFFF, Jade #AEB8A0, Plum Noir #351E28, Wasabi #E9F056 and Persimmon #FF5C34. Only Plum Noir is dark, so it has to carry the text. Measured against the other four:

  • Plum Noir on Cool Blue: 12.93:1
  • Plum Noir on Wasabi: 12.48:1
  • Plum Noir on Jade: 7.43:1
  • Plum Noir on Persimmon: 4.995:1

All four pass for body text, the last one by a small margin. The tempting mistake is white text on Persimmon, because a bright orange button with a white label looks right. It measures 3.07:1, which is fine for a large headline and not for a 16 px button label. So the button on the library page uses a Plum Noir label.

The second mistake is easy to miss because it is not text: the outline of an email field. If you draw it in Jade on a Cool Blue page, it measures 1.74:1, well under the 3:1 that 1.4.11 asks for, and people with low vision will not see where the field is. Draw the outline in Plum Noir and keep Jade for a divider line, which is decoration and has no requirement.

Plum Noir #351E28 text measured on the other four 2026 Pinterest Palette colours, all passing 4.5:1 for body text: 12.93:1 on Cool Blue #D7EFFF, 12.48:1 on Wasabi #E9F056, 7.43:1 on Jade #AEB8A0 and 4.995:1 on Persimmon #FF5C34. Below, the two mistakes and their fixes on a Cool Blue page: a white label on a Persimmon button at 3.07:1, which fails for a 16 px label and suits large text only, against a Plum Noir label at 4.995:1, which passes; and an email field outlined in Jade at 1.74:1, which fails the 3:1 an outline needs, against a Plum Noir outline at 12.93:1, which passes

Check 2: the autumn palette, and a pair we got wrong

The autumn palette is Mulled Wine #7A2E3A, Rust #B4532A, Harvest Gold #D8A13A, Moss #5F6B3A and Oat Milk #EDE0C8. Until 27 October its page said that for body text you could "stay on Oat Milk, where both dark colours pass easily", meaning Mulled Wine and Moss.

Measured, only one of them does:

  • Mulled Wine on Oat Milk: 7.06:1. Passes, even at AAA.
  • Moss on Oat Milk: 4.41:1. Fails for body text.

Moss looks dark. It is a deep olive and it reads as "dark enough" at a glance. But it is a green, and green carries most of the luminance, so it sits closer to the cream than it looks. At 4.41:1 it passes for large text only, so it is fine for a 24 px heading and wrong for the paragraph under it. We corrected the page and added a worked example (a cafe menu card) that uses Moss only for headings.

The same cafe menu card on Oat Milk #EDE0C8 three ways. All in Mulled Wine #7A2E3A: 7.06:1, passing 4.5:1 for body text and 7:1 for AAA. All in Moss #5F6B3A: 4.41:1, failing 4.5:1 for body text and passing 3:1 for large text. The fix: a 24 px heading in Moss at 4.41:1, which counts as large text and passes, with the 16 px text in Mulled Wine at 7.06:1

The lesson is general: judging contrast by eye fails most often with greens and with saturated mid tones. Measure the pairs that "obviously" pass, too.

Check 3: a dashboard that fails by a hair, and a chart that fails by a lot

The calm dashboard uses eight tokens, including Sage #4E7A5B for good news and Clay #B4553A for bad news, shown as small "+12%" and "−2%" figures on white cards.

  • Sage on the white card: 4.93:1. Passes.
  • Clay on the white card: 4.88:1. Passes.
  • Sage on the page background #F6F4EF: 4.486:1. Fails.

That last one is the 4.499 case from the WCAG note in real life. Move the figure off the card onto the page, and a colour that passed now fails, because the off white background is a touch darker than white. Rounded to one decimal it shows as 4.5, which is exactly why you keep the third decimal.

The bar chart on the same page is a clearer failure. The bars are Soft blue #7BA3C9 at 85% opacity on white. At full opacity that blue is 2.65:1 against white, and the transparency only lowers it. If the bars are the only way to read the numbers, 1.4.11 asks for 3:1. There are two fixes: print each value above its bar, so the bars become decoration, or darken them. #5B87B5 measures 3.77:1 on white and still looks like the same soft blue family.

The calm dashboard's figures and bars. A +12% figure in Sage #4E7A5B on the white card #FFFFFF measures 4.93:1 and a minus 2% figure in Clay #B4553A on the white card 4.88:1, both passing 4.5:1. The same Sage figure on the page background #F6F4EF measures 4.486:1, which shows as 4.5 when rounded and fails. Beside them, the weekly bar chart on white: Soft blue #7BA3C9 bars at 85% opacity, 2.65:1 at full opacity, fail the 3:1 chart marks need, and darker #5B87B5 bars at 3.77:1 pass

Check 4: a success state that misses

Our slide to confirm control turns green when it is done and shows the word "Sent" in cream #FDF9F1 on green #1E8A50. That pair measures 4.16:1. For a bold word that is not large enough to count as large text, it falls short.

Greens are the usual culprit again. A deeper green, #17703F, measures 5.84:1 with the same cream and still reads as "done". We kept the demo's colour on the page so it matches the pin, and put the fix in the page's worked example.

The slide to confirm control in its done state, the word Sent in cream #FDF9F1 on green, twice. On the demo's green #1E8A50 it measures 4.16:1 and fails 4.5:1 for normal text. On a deeper green #17703F it measures 5.84:1 and passes

Check 5: why no text colour survives a moving sky

The hillside town loop runs through four skies: dawn #EB9EA8, day #56B4E6, dusk #BB557B and night #1C2758. Suppose you want to put a "Starting soon" title straight on it. Which colour should the title be?

There isn't one, and the reason is neat. Contrast ratios multiply. If a text colour has 4.5:1 against a light background and 4.5:1 against a dark one, then the two backgrounds must be 4.5 × 4.5 = 20.25:1 apart. The lightest sky (dawn) against the darkest (night) is only 6.75:1. So any colour that passes on dawn fails on night, and the other way round.

The same arithmetic applies to every background that changes: photos, videos, gradients. Our moving gradient wallpaper goes from a peach #FFD2BD, where ink text #17202C scores 11.88:1, to a violet #7A5CFF, where the same ink scores 3.75:1. The fix is always the same: give the text its own solid box, and measure the text against the box.

The title Starting soon in white #FFFFFF and in ink #17202C on the hillside town loop's four skies, checked against 4.5:1. Dawn #EB9EA8: white 2.10:1 fails, ink 7.81:1 passes. Day #56B4E6: white 2.32:1 fails, ink 7.09:1 passes. Dusk #BB557B: white 4.47:1 and ink 3.67:1 both fail. Night #1C2758: white 14.18:1 passes, ink 1.16:1 fails. Dawn against night is only 6.75:1, while passing 4.5:1 on both would need 4.5 times 4.5, 20.25:1. The fix: the title in ink on a solid cream #FDF9F1 box across all four skies, 15.63:1 against the box. Underneath, the gradient wallpaper: ink scores 11.88:1 on the peach and 3.75:1 on the violet

A routine that catches all of this

  1. Write down the pairs you use, not the palette. Body text on page, body text on card, button label on button, link on page, placeholder on field, field outline on page, focus ring on page, chart marks on card.
  2. Measure each one with the function above or any checker that uses the WCAG formula.
  3. Mark each pair with its job. Body text needs 4.5:1, large headings 3:1, outlines and chart marks 3:1. A pair can be fine for one job and wrong for another, like Moss on Oat Milk.
  4. Keep three decimals near the line. 4.486 is not 4.5.
  5. Check every state. Hover, focus, selected, done and error colours are new pairs. The success green in check 4 only appears after the action.
  6. Give text a box on anything that moves. If the background changes, measure against the box.

What contrast does not cover

A pair that passes can still fail people in other ways:

  • Colour alone. WCAG 1.4.1 says colour should not be the only way information is shown. Red and green "down" and "up" figures need the minus and plus signs too, which the dashboard has. Our tick and cross toggle switch shows a cross or a tick on the knob for the same reason.
  • Thin type. The ratio assumes ordinary strokes. A hairline serif like Instrument Serif at 18 px reads worse than the number suggests, which is why the library keeps it at 40 px and up.
  • Long reading. For comfortable reading, WCAG's AAA guidance asks for lines no wider than 80 characters and line spacing of at least one and a half times the text size (1.4.8). Contrast gets the letters seen. Spacing gets them read.

Every palette page in the library lists the pairs that pass, with their ratios, strongest first. If one of them is wrong, tell us at support@sundrymill.com and we will fix the page, as we did with the autumn one.

Sources

More from the blog

All posts RSS feed