What Color Should Your Body Text Be?
Pure black. #000000. It's the default body text color in every browser stylesheet and the first thing a lot of "never use pure black" advice tells you to change, without ever saying what to change it to. The short answer: the best body text color for readability sits in a narrow gray band, roughly #1a1a1a to #333333, and whichever exact shade you land on still has to clear WCAG's 4.5:1 contrast minimum against your actual background.
This isn't a stylistic preference. Pure black on white produces the single biggest brightness jump a screen can render for text, a 21:1 contrast ratio, the ceiling of the scale. Your eyes re-adjust every time they cross from the white margin into a black letterform, and across a full paragraph that shows up as fatigue, not sharpness. Pull the black back a little and the words get easier to sit with.
Why pure black on white feels harsh
The effect has a name: halation. Extreme brightness contrast makes the eye's aperture work harder, and for a meaningful share of readers, especially anyone with astigmatism, that shows up as edges that seem to shimmer or bleed. It's the same reason print designers have avoided true black ink on true white stock for a long time, reaching instead for a warm near-black on an off-white page.
Here's the number that makes this easy to reason about: black on white measures 21:1, and WCAG's minimum for normal body text is 4.5:1. That's roughly 4.6 times more contrast than accessibility guidelines ever ask for. All of that extra headroom is exactly what you get to spend on a softer color without any risk of failing the spec.
The body text color range that actually works
Most systems that have thought hard about this land somewhere between #1a1a1a and #333333. Google's Material Design spec puts a number on it directly: primary, high-emphasis text is black at 87% opacity over a white surface, which works out to a flat #212121, a color you'll recognize from a lot of Android and web apps built on that system.
Tailwind CSS ships a similar idea under a different name. Its default gray-900 is #111827, a near-black with a faint blue undertone that shows up as the default body copy color on a large share of the sites built with it. None of these are the "correct" hex; they're proof that independent teams converged on the same idea from different directions: dark gray, not black.
:root{
--text-primary: #1a1a1a;
--text-secondary: #4b4b4b;
--text-muted: #6b6b6b;
}
body{ color: var(--text-primary); } Set that as a starting point, then check it. The range is a shortlist, not a guarantee.
The contrast trap: your gray still has to pass 4.5:1
Going darker than pure black obviously isn't possible, but going too light in the other direction is the mistake people actually make once they hear "dark gray text" and start reaching for anything that isn't black. #999999 on white measures just 2.85:1, a clear WCAG AA fail for normal text, and even short of the looser 3:1 floor for large text. #767676 lands right at 4.54:1, about as light as a gray can get on a white background before it fails.
Don't eyeball this part. Open the WCAG Contrast Checker and drop in the exact hex you're about to ship against your exact background. It flags AA and AAA pass or fail instantly, live, before the color ever reaches your CSS. If you want the fuller picture of what those 4.5:1, 3:1 and 7:1 numbers actually mean and when each one applies, the color contrast and WCAG guide walks through it in plain terms.
Quick habit: any time you pick a gray by eye, for body copy, a caption, a placeholder, run it through the contrast checker before it lands in production. It takes five seconds and it catches the failures that "looks fine to me" never does.
Body text, headings and muted text aren't the same role
A color system needs more than one gray. Body copy usually sits in that #1a1a1a-#333333 band because it's small and read for a long stretch. Headings can often go a shade darker or use full black safely, since large, bold type is far more forgiving of high contrast, the halation problem is mostly a small-type problem. Secondary text, timestamps, captions, helper copy, can step lighter for visual hierarchy, but only down to whatever hex still clears 4.5:1; if it's meaningful text and not purely decorative, the contrast rule doesn't relax just because the role changed.
The fastest way to see whether a whole system holds together is to stop checking one pair at a time. Open UI Color Preview and drop your primary, secondary and muted text colors into real buttons, cards, badges and nav. Problems that never show up in an isolated swatch tend to jump out the moment text sits inside an actual component. The accessible color palette guide covers the rest of that system: pairing, color-blindness testing, and not relying on color alone to carry meaning.
Dark mode flips the problem, it doesn't erase it
The instinct in dark mode is to invert everything: black background, white text, done. But pure #ffffff on a near-black surface is the same halation problem run in reverse, and it produces the same 21:1-scale extreme if the background is also true #000000. Soften both ends instead. A near-black surface like #121316 paired with an off-white text color like #e8e9ec measures about 15.3:1, comfortably past even the strict 7:1 AAA threshold, with none of the glare of the pure combination.
Dark mode isn't just light mode with the values swapped; it needs its own surfaces, its own accent saturation, and its own text hierarchy. The dark-mode color palette guide covers the rest of that, why pure black backgrounds fail the same way pure white ones do, and how to pick accents that don't glare once the surface goes dark.
Test it on your own font, not a generic one
A contrast ratio is math, but readability is also about the actual typeface. A hairline serif at 15px reads differently than a chunky grotesk at the same size and the same hex, and the gray that felt perfect in a design comp can feel thin or muddy once it's rendered in your real font stack at your real body size.
Open the Typography Tester, load your own Google or system font, and drop in a candidate body text color against your real background. You'll see it rendered at actual size and line height, not a demo screenshot.
Pick a gray in the #1a1a1a-#333333 range, confirm it clears 4.5:1 against your exact background, and keep light and dark mode consistent with the same logic instead of a straight inversion. That's the whole decision, worth five minutes with a checker instead of a guess that ships to every reader of the page.
Common questions
Should text ever be pure black (#000000)?
Rarely. Pure black passes contrast easily, 21:1 on white, far above WCAG's 4.5:1 minimum, but that maximum brightness jump is exactly what causes halation and eye fatigue over a long paragraph. A dark gray in the #1a1a1a-#333333 range reads just as clearly with less strain. The exception is print or brand assets that specifically call for true black ink.
What's the best text color for readability on a white background?
There's no single winning hex, only a range. Most systems land between #1a1a1a and #333333, with Material Design's #212121 (black at 87% opacity) as a reasonable default. Whichever one you pick, verify it against your real background with a contrast checker rather than trusting the range alone; some sub-ranges of gray still fail 4.5:1.
Does dark mode use the same gray range for text?
No, invert the logic, not the exact hex. Instead of pure #ffffff on pure #000000, pull primary text to an off-white like #e8e9ec and place it on a near-black surface like #121316. Same principle as light mode, soften both ends, applied in the opposite direction.
Ready to try it? Every tool on Paleta runs free in your browser — no sign-up, nothing uploaded.
Explore the tools →