Color Contrast People Can Actually Read
Quick checks for text and UI colors that stay legible in light mode, dark mode, and on mediocre displays.
Color Contrast People Can Actually Read
Good contrast is not about passing a checklist once—it is about text that stays readable on real screens, in sunlight, and when someone bumps their OS into high-contrast mode.
Start with body text, not the hero gradient
Pick foreground and background pairs for paragraphs and buttons first. Decorative gradients can be looser; anything people read for more than a second should hit WCAG AA at minimum (4.5:1 for normal text, 3:1 for large text).
Browser DevTools and Figma both include contrast checkers. Use them on the actual font weight and size you ship, not a generic sample. Thin gray text at 14px on white may “look elegant” in a mock and fail hard on a dim laptop.
Practical order of decisions:
- Page background and body text — the pair users spend minutes with
- Primary buttons — label vs fill, and fill vs surrounding page
- Links in body copy — must be distinguishable from surrounding text
- Secondary chrome — nav, captions, placeholders (placeholders often fail; do not use them as the only label)
- Decorative layers — gradients, mesh, blur—behind text only if the text still passes on the effective background
:root {
--bg: #f6f4ef;
--text: #1c1917;
--muted: #57534e; /* check muted against --bg; “muted” is not an excuse to fail */
--accent: #0f766e;
--accent-text: #ecfdf8;
}
body {
background: var(--bg);
color: var(--text);
}
a {
color: var(--accent);
}
.button-primary {
background: var(--accent);
color: var(--accent-text);
}
Store pairs as tokens, not one-off hex values sprinkled across components. When contrast fails, you fix a token once instead of hunting twenty copies of “almost the same gray.”
Large text and bold weights change the math—confirm with the real computed size. Icons that carry meaning (error, success) need contrast too, not only the adjacent label.
Do not rely on color alone
Links, errors, and selected states need a second signal—underline, icon, weight, or border. That helps color-blind users and anyone on a dim monitor where teal and gray collapse together.
Patterns that hold up:
- Links — underline in body text (or a clear weight/icon treatment that is not color-only)
- Errors — text message plus icon; red border alone is not enough
- Charts — patterns or labels, not only hue series
- Required fields — text (“required”), not only a red asterisk with no legend
- Selected tabs — underline or marker shape in addition to color
.prose a {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 0.15em;
}
.field-error {
border-color: #b91c1c;
}
.field-error-message {
color: #991b1b;
}
Focus rings are part of this story. A 2px accent outline with enough contrast against both the control and the page background keeps keyboard users oriented. Do not make focus color-only and low-contrast.
Test both themes
Dark mode is not “invert and hope.” Muted grays that work on white often disappear on near-black backgrounds. Re-check contrast when you toggle prefers-color-scheme or a manual theme switch.
@media (prefers-color-scheme: dark) {
:root {
--bg: #0c0a09;
--text: #f5f5f4;
--muted: #a8a29e;
--accent: #2dd4bf;
--accent-text: #042f2e;
}
}
Dark-theme gotchas I keep hitting:
- White text on saturated brand blues that looked fine as light-theme buttons
- Borders at
#333on#111that vanish, killing card structure - Overlay scrims that are too light, so modal text fails against busy backgrounds
- Shadows used as the only separator—useless when contrast is already low
Test on a real phone outdoors or near a window once. Lab AA passes can still feel weak on a washed-out panel. If you ship a manual theme toggle, test both themes with the same components—nav, post body, forms, and code blocks. Syntax-highlighted code is a frequent dark-mode failure: theme packs assume a specific background.
Forced colors / Windows high contrast modes deserve a quick pass. Prefer semantic tokens and system colors where appropriate so UI does not disappear when the OS overrides your palette.
Build contrast into the workflow
Contrast fixed at the end of a project regresses immediately. Bake checks into design and review:
- Define text/background tokens before exploring decorative palettes
- Run DevTools contrast on new components in PR review
- Include a light/dark screenshot pair for UI changes that touch color
- Avoid “placeholder gray” for anything essential
If you use a design tool, set styles for Body, Muted, and Inverse text and reuse them. Random hex from a gradient generator is how portfolios ship beautiful, unreadable captions.
Wrap-up
Readable color is a product decision: check contrast on real UI elements, support non-color cues, and validate every theme you ship. Start with body text and controls, not the hero gradient, and treat tokens as the source of truth. It is one of the highest-impact accessibility fixes you can make in an afternoon—and one of the easiest to keep if you stop treating contrast as a last-mile polish pass.