I shipped a dark theme two years ago that looked flawless in the Figma file and slightly wrong in production. Same font, same weight value, same hex codes I'd tested against a contrast checker. It just looked heavier than it should, almost bold where I'd specified regular. It took me an embarrassingly long afternoon to realize the problem wasn't a bug. It was how light text behaves on a dark background, full stop.
That's the part most teams miss when they add dark mode as an afterthought: swapping background and text colors isn't a typography-neutral operation. The same weight, size, and spacing values that read correctly in light mode will not automatically read correctly once you invert them.
TL;DR: Light text on dark backgrounds visually appears heavier due to an optical effect called irradiation, a Medium 500 weight can read like Semibold 600. Increasing font weight by one step and line-height by 10-20% is the standard 2026 fix, alongside avoiding pure white on pure black (Mark Boulton, 2026).
Why Does Light Text Look Heavier on Dark Backgrounds?
It's a real optical phenomenon, not a rendering bug, called irradiation. Bright shapes on a dark field appear to expand slightly beyond their actual edges, an effect photographers and typographers have documented for decades, long before dark mode UI existed. Your eye perceives the white letterforms as bleeding a little into the surrounding black.
Practically, this means a heading set at font-weight 600 in light mode can look noticeably heavier, sometimes closer to 700, once you flip to a dark background at the same pixel size. Designers who don't account for this end up with dark themes that feel clumsy or over-bold compared to their light counterparts, even though the numeric weight value never changed.
| Property | Light mode | Dark mode adjustment |
|---|---|---|
| Heading weight | e.g. 700 | Drop one step, e.g. 600 |
| Text color | Pure white acceptable | Off-white (#E8E8E8-#EDEDED), not pure white |
| Background | Pure black acceptable | Dark gray (#121212-#1A1A1A), not pure black |
| Line-height | Baseline value | 10-20% higher |
How Should You Adjust Font Weight for Dark Themes?
Don't share one weight token across both themes. Set up separate CSS custom properties, --font-weight-heading-light: 700 and --font-weight-heading-dark: 600, and let your theme switcher pick the right one. I know that sounds like duplicated work, but the alternative is a dark theme that consistently reads slightly bolder than intended across every single heading on the site.
:root[data-theme="light"] {
--font-weight-heading: 700;
--font-weight-body: 400;
}
:root[data-theme="dark"] {
--font-weight-heading: 600;
--font-weight-body: 400;
}
h1, h2, h3 { font-weight: var(--font-weight-heading); }
Body text needs less adjustment than headings in my experience, the irradiation effect is more visible at larger sizes and heavier weights. A 400-weight body paragraph rarely needs a dark-mode-specific override. It's your 600 and 700-weight headings that drift.
What Is Optical Sizing and Why Does It Matter Here?
Optical sizing is a variable font axis (opsz) that reshapes a typeface's proportions, stroke contrast, x-height, spacing, based on the point size it'll render at, rather than just scaling one fixed shape up or down. High-contrast display faces (thin strokes paired with thick ones) are the ones that suffer most on dark backgrounds; those thin strokes can look like they're flickering or dissolving against black, an effect that gets worse the smaller the text gets.
Fraunces and Recursive both ship with a usable opsz axis you can dial toward a heavier setting specifically for dark mode headings, without touching the numeric font-weight value at all:
h1 {
font-variation-settings: "opsz" 72, "SOFT" 30;
}
If your chosen typeface doesn't include an optical sizing axis, the practical substitute is simple, bump the font-weight token by one increment for dark mode, as covered above. It's not as elegant as a purpose-built optical axis, but it solves the same visual problem for teams working with static font families.
How Should Contrast Ratios Differ From Light Mode?
Higher contrast is not automatically better, which surprises people the first time I say it out loud. Pure white (#FFFFFF) text on pure black (#000000) technically maximizes your contrast ratio, but research on reading comfort suggests that extreme polarity like this can slow reading speed by as much as 20% compared to a softer pairing. It's genuinely more tiring to read for long stretches.
Use an off-white body color, something in the #E8E8E8 to #EDEDED range, against a dark gray rather than true black, somewhere around #121212 to #1A1A1A. You'll still clear WCAG AA's 4.5:1 contrast requirement comfortably, and the reading experience improves noticeably over a 20-minute session. WCAG 3.0's newer APCA model actually scores perceptual contrast more precisely than the old ratio-based check, factoring in font size and weight alongside polarity, worth tracking as it moves toward wider adoption (Inkbot Design, 2026).
Does Line Height Need to Change Too?
Yes, modestly. Bump your line-height by roughly 10-20% for dark mode body text compared to light mode. A paragraph sitting comfortably at 1.5 line-height in light mode often wants 1.6 to 1.65 in dark mode. The irradiation effect makes lines of text feel like they're crowding each other even when the actual pixel spacing hasn't moved a single unit.
Store this as its own token rather than burying it in a one-off override class. If you're building out a broader dark theme beyond typography, our dark mode UI design guide covers the color system and component-level decisions that pair with these type adjustments.
Getting dark mode typography right is mostly a discipline of not assuming light-mode values transfer cleanly. They don't, not for weight, not for contrast, not for line-height. Test every type decision against the actual dark background you shipped, not the one you assumed would behave the same as light mode inverted.