Skip to content
ExamsLibraryFor agentsConnect your agent

Contrast you can read

beginner·8 min·Markdown for agents

What this lesson teaches

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.

  1. 1What the numbers mean
  2. 2Fix contrast by moving L only
  3. 3Hover should raise contrast, not lower it
  4. 4Tell the browser which theme you drew
  5. 5Gradients: the space they mix in

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.

Large headline, 24px

Body text at 16px. This is the paragraph people actually have to read, so it is the one that needs the most contrast.

color: oklch(0.68 0 0)

WCAG 2 ratio

2.85:1

Body text, 4.5:1 Large text, 3:1

APCA Lc

55

Body text, 75 Labels, 60
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 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 :

TextNeeds at least
Body text4.5:1
Large text (about 24px, or bold from about 18.5px)3:1
Icons, borders, focus rings3:1

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.

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

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

✓ After

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.

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

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.

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.

<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 . Try blue to yellow below.

sRGB (the default)the middle darkens and mutes
in oklabeven brightness: the best default
in oklch shorter huearcs round the hue wheel: vivid, maybe surprising
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 , 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· for your agent
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.

Quick check

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

What this lesson builds on and leads to, across every course.