# Contrast you can read

> Check text against 4.5:1 and APCA, repair a failing pair by moving lightness only, make hover states raise contrast, and pick the space a gradient mixes in.

Module: Color, depth and surfaces · Level: beginner · 8 min · https://school.agents.org.in/design/surface/contrast

The gray in this box is `#999` on white, the most popular "subtle" text color on the web. Read the paragraph, then look at the score.

*[Interactive demo: Drag the text's lightness, or press the fix. Only L moves.]*

2.85:1. It fails. Light gray on white looks calm and refined in a mockup, and on a sunny phone or a tired pair of eyes it simply isn't there.

## What the numbers mean

A contrast ratio (How different the text color is from its background, as a number like 4.5:1. Body text needs at least 4.5:1 to be readable for most people; large text 3:1) compares how light the text is with how light its background is. 1:1 is invisible; 21:1 is black on white.

The rule most laws point to comes from WCAG (The Web Content Accessibility Guidelines, the standard most laws point to. Its contrast rule is the familiar 4.5:1 for body text and 3:1 for large text):

| Text | Needs at least |
|---|---|
| Body text | **4.5:1** |
| Large text (about 24px, or bold from about 18.5px) | **3:1** |
| Icons, borders, focus rings | **3:1** |

APCA (A newer contrast formula that models how people actually perceive text, scored as Lc: about 75 for body text, 60 for labels, 45 for headlines) is a newer formula that tracks how people actually perceive text, especially light text on dark. It scores in **Lc**: 75 or more for body text (90 is better), 60 for labels, 45 for headlines, 30 as the floor for icons and disabled states. The skill's advice: **run both, pass both.**

> **In plain words.** WCAG's 4.5:1 is the legal line. APCA is the better eye test. A color pair that clears both reads well for nearly everyone.

## Fix contrast by moving L only

Here is where OKLCH from the first lesson pays off. Contrast lives almost entirely in the gap between the two **lightness** values. Chroma and hue barely move it.

So to repair a failing pair, push the text's L away from the background's L and leave C and H alone. "Make it more saturated so it pops" does nothing for readability.

A fast triage before you measure:

- On near-white backgrounds (L 0.85 and up), text wants L 0.45 or below.
- On near-black backgrounds (L 0.25 and down), text wants L 0.75 or above.
- Anything with L above about 0.6 wants dark text on it.

  **Before (avoid):**

`color: #999;` on white. 2.8:1, fails body text.

  **After (do this):**

`color: oklch(0.5 0 0);` on white. 6:1, passes. Same hue, same chroma, lower L.

Two more traps:

- **No text on a see-through color.** A color with alpha mixes with whatever is under it, so no single measurement describes it. Text sits on solid tokens.
- **Text on a gradient** has a different contrast at every pixel. Measure the worst spot, not the average, or put a solid scrim behind the words.

## Hover should raise contrast, not lower it

A common reflex is to fade a button on hover. Backwards. The skill's rule: hover makes an element a little *more* contrasty, pressed more again. **Disabled is the only state that lowers contrast**, and it does so with its own tokens, not opacity.

```css
.button        { background: var(--bg-elevated);        border-color: var(--border-default); }
.button:hover  { background: var(--bg-elevated-hover);  border-color: var(--border-strong); }
.button:active { background: var(--bg-elevated-active); }
.button:disabled { background: var(--bg-disabled); color: var(--text-disabled); }
```

## Tell the browser which theme you drew

Two small declarations make the browser's own parts match your page.

`color-scheme` (A CSS property that tells the browser whether to draw its own parts (scrollbars, form controls) light or dark. Declare only the themes you actually designed) tells it whether to draw scrollbars, checkboxes and `<select>` menus light or dark. Declare only what you designed: a light-only site writes `color-scheme: light`. Writing `light dark` on a light-only site hands dark-mode users dark native dropdowns on a white page.

`theme-color` (A meta tag that colors the phone browser's address bar and status bar so they blend with the top of your page) colors the phone's address bar to match the top of your page, so your design doesn't stop abruptly at a gray browser strip.

```html
<meta name="theme-color" content="#fdfdfc">
```

## Gradients: the space they mix in

The same two colors make three different gradients, depending on the color space the browser mixes them in. That is gradient interpolation (The color space a gradient mixes its stops in. The same two colors make a muddy, an even, or a vivid gradient depending on it (sRGB, OKLab or OKLCH)). Try blue to yellow below.

*[Interactive demo: Same stops, three spaces. Blue to yellow goes gray in the middle of sRGB.]*

- **sRGB**, the default, darkens and muddies the middle.
- **`in oklab`** keeps brightness even across. It is the best default.
- **`in oklch`** travels round the hue wheel, so it stays vivid, and may pass through a hue nobody asked for (blue to pink goes via purple). Add `longer hue` and it sweeps most of the rainbow.

Colors on opposite sides of the wheel, like blue and yellow, pass near gray in a straight-line space. Either switch to `in oklch` or add a third stop between them.

Large, low-contrast gradients can also show banding (Visible stripes in a gradient that should be smooth, common on large, low-contrast gradients. A faint noise layer hides it), visible stripes where it should be smooth. Widen the gap between stops, shrink the area, or lay a faint noise texture over it. *(Picking the space is taste. A gray dead zone, banding, or unreadable text on the gradient is a bug.)*

**Audit contrast (paste into your agent):**

```text
Audit text contrast in this project.
- For every text/background pair, report the WCAG 2 ratio and the APCA Lc. Body text must pass 4.5:1 and Lc 75; labels Lc 60; large text 3:1.
- Fix failures by moving the foreground's OKLCH L only; leave C and H.
- Hover and active states must raise contrast, never lower it. Disabled uses its own tokens, not opacity.
- No text on colors with alpha. Set color-scheme to only the themes we actually ship.
- Gradients: use "in oklab" unless the design wants a hue sweep; flag any with text whose worst region fails.
```

**Check yourself:** Gray caption text measures 3.1:1 on white. What's the right repair?

- [ ] Increase its chroma so it pops
- [ ] Make the font a little bolder
- [x] Lower its OKLCH L until it passes 4.5:1, keeping C and H
- [ ] Add a text-shadow

*Why:* Contrast lives in the lightness gap. Moving L fixes it; chroma, hue and effects barely change the score.

## In the skill

- https://school.agents.org.in/skill/surface/contrast-and-color-scheme.md
- https://school.agents.org.in/skill/surface/gradient-interpolation.md
