# Color contrast: readable text for everyone

[Canonical HTML page](https://gradiently.design/guide/color-contrast-accessibility)

Contrast is the difference between words people read and words people skip. The WCAG numbers in plain English, how to check them, and what changes when the background is a gradient.

## The short version

- Color contrast is the difference in brightness between text and its background, measured as a ratio from 1:1 (no difference) to 21:1 (black on white).
- WCAG 2 level AA asks for at least 4.5:1 for normal text and 3:1 for large text, which is about 24 px regular or 18.66 px bold.
- Icons, input borders and other parts of an interface people need to see also need at least 3:1 against what surrounds them.
- On white, `#767676` is the lightest plain grey that passes 4.5:1 for body text, and `#777777` just fails.
- On a gradient, contrast changes from point to point, so you must test the worst spot behind the words, not the average colour.

**Color contrast** is the difference in brightness between text and the colour behind it, and it decides whether people can read your words at all. The Web Content Accessibility Guidelines (WCAG) measure it as a ratio from 1:1, where the two colours are identical, to 21:1, black on white. For level AA, normal text needs at least **4.5:1** and large text at least **3:1**. Everything else in this guide is about hitting those numbers without making your design timid.

Contrast matters for everyone, not only for people with low vision. Phones in sunlight, dim projectors, cheap monitors and tired eyes all shrink the difference between colours. Designs with enough contrast simply get read more.

## The WCAG contrast ratios in plain words

Three rules in WCAG 2 cover almost every design decision. They come from the [W3C's success criteria](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html), and they are what most accessibility audits and regulations refer to.

| What | Level AA | Level AAA | Notes |
| --- | --- | --- | --- |
| Normal text | 4.5:1 | 7:1 | Body copy, captions, labels, buttons |
| Large text | 3:1 | 4.5:1 | At least 18 pt (about 24 px), or 14 pt bold (about 18.66 px) |
| Icons and interface parts | 3:1 | 3:1 | Input borders, chart lines that carry meaning, icons without labels |
| Logos and decoration | None | None | Brand logotypes and purely decorative text are exempt |

Level AA is the usual target. AAA is stricter and worth aiming for in long reading.

Two details catch people out. First, "large" is about the rendered size, so a 24 px heading on a phone that scales it down may stop counting as large. Second, a disabled button is exempt, but a light grey placeholder that people need to understand is not.

## How the contrast ratio is calculated

The ratio compares the relative luminance of the two colours: how much light each one gives off, weighted the way the eye sees. Green contributes most, red less and blue very little, which is why a saturated blue can be surprisingly dark and a yellow surprisingly light. The formula is `(L1 + 0.05) / (L2 + 0.05)`, where L1 is the lighter colour.

```js
// WCAG 2 contrast ratio between two hex colours
const channel = (c) => {
  c /= 255
  return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
}
const luminance = (hex) => {
  const [r, g, b] = [1, 3, 5].map((i) => channel(parseInt(hex.slice(i, i + 2), 16)))
  return 0.2126 * r + 0.7152 * g + 0.0722 * b
}
const contrast = (a, b) => {
  const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x)
  return (hi + 0.05) / (lo + 0.05)
}

contrast('#767676', '#ffffff') // 4.54, passes for body text
contrast('#777777', '#ffffff') // 4.48, fails
```

The whole calculation fits in a dozen lines. Useful for checking a brand palette in bulk.

You do not need to calculate it by hand. The [WebAIM contrast checker](https://webaim.org/resources/contrastchecker/) takes two hex codes and tells you which levels pass, and the colour picker in Chrome's developer tools shows the ratio of any text you inspect.

## Colour pairs that pass and fail

Brand colours often sit in the awkward middle, too dark for dark text and too light for white text. Medium greens, oranges, pinks and light blues are the usual culprits. These are common pairings, with their real ratios.

| Text on background | Ratio | Normal text | Large text |
| --- | --- | --- | --- |
| White on `#7c3aed` violet | 5.7:1 | Pass | Pass |
| White on `#8b5cf6` light violet | 4.2:1 | Fail | Pass |
| White on `#2563eb` blue | 5.2:1 | Pass | Pass |
| White on `#3b82f6` sky blue | 3.7:1 | Fail | Pass |
| White on `#10b981` green | 2.5:1 | Fail | Fail |
| `#0b0b10` near-black on `#10b981` green | 7.7:1 | Pass | Pass |
| White on `#f59e0b` amber | 2.2:1 | Fail | Fail |
| `#999999` grey on white | 2.9:1 | Fail | Fail |
| `#6b7280` grey on white | 4.8:1 | Pass | Pass |

A small shift can decide it: two blues that look almost alike land on opposite sides of 4.5:1.

> **Flip the text, not the brand** When white fails on a bright brand colour, try near-black text instead. Green, amber and coral usually pass comfortably with dark type, and the colour stays exactly as it is. [Choosing brand colours](https://gradiently.design/guide/how-to-choose-brand-colors) covers building this into a palette from the start.

## Why gradients make contrast harder

A flat background has one contrast ratio. A gradient has a different one at every point. White text that passes easily over the dark end of a blend can fail completely at the light end, and a headline that stretches across the gradient passes and fails in the same line.

Midnight to violet to pink: `linear-gradient(90deg, #1f0b3a 0%, #7c3aed 55%, #f472b6 100%)`

White text scores 18:1 at the left, 5.7:1 over the violet and 2.7:1 at the pink end. One gradient, three verdicts.

The rule is simple: test the lightest point behind light text, or the darkest point behind dark text. If the worst point passes, the whole line passes. Average colours and eyedropper samples from the middle will mislead you.

## How to fix contrast on a gradient

1. **Move the words to the calm end** Place text over the darkest or lightest part of the gradient, and let the bright transition happen away from it.
2. **Narrow the range behind the text** Keep the part of the gradient that sits behind words within a small lightness range. The rest of the canvas can be as bold as you like.
3. **Add a soft scrim** A gentle dark or light fade behind the text, such as a transparent-to-30% black gradient, lifts contrast without a visible box.
4. **Go bigger and bolder** Large text only needs 3:1. A heavier weight and a larger size can turn a failing line into a passing one.
5. **Test on a real phone** Check the design at full brightness and at low brightness, outdoors if you can. Ratios are the floor, not the goal.

```css
.card {
  background: linear-gradient(90deg, #1f0b3a 0%, #7c3aed 55%, #f472b6 100%);
  color: #ffffff;
}
/* A soft scrim keeps white text above 4.5:1 across the pink end */
.card .text {
  background: linear-gradient(90deg, transparent 0%, rgb(31 11 58 / 0.55) 60%);
  padding: 1rem 1.25rem;
}
```

The scrim darkens only where the gradient gets light, so the left side stays untouched.

A portrait post where the headline Doors open at seven crosses a bright area of the gradient and becomes hard to read

The words run straight over the brightest part of the field and their contrast drops.

The same portrait post with a darker, calmer area of the gradient behind the headline so it reads clearly

The same design with the calm, dark region moved behind the words.

This is the idea behind Gradiently's legibility: a Mark knows where your words are, moves its calmest, best-contrasting region behind them and picks light or dark ink per line from its own palette. For more techniques that work anywhere, read [text on a gradient](https://gradiently.design/guide/text-on-gradient) and [readable text over photos](https://gradiently.design/guide/text-over-image). For dark interfaces specifically, see [designing for dark mode](https://gradiently.design/guide/dark-mode-design).

## FAQ

### What is a good color contrast ratio?

At least 4.5:1 for normal text and 3:1 for large text meets WCAG level AA. For long reading, 7:1 (level AAA) is more comfortable.

### What counts as large text in WCAG?

Text of at least 18 point, about 24 px, or at least 14 point bold, about 18.66 px. Large text only needs 3:1 at level AA.

### What is the lightest grey I can use on white?

`#767676` is the lightest plain grey that reaches 4.5:1 on white. `#777777` measures 4.48:1 and fails for body text.

### Do logos need to meet contrast requirements?

No. WCAG exempts text that is part of a logo or brand name, and purely decorative text. Everything people need to read or use still counts.

### How do I check contrast on a gradient background?

Measure the text colour against the lightest point behind light text, or the darkest point behind dark text. If that worst point passes, the whole line passes.
