Skip to content

How Typography Changes When You Switch to Dark Mode

Why the same font weight looks different on a dark background, and how to adjust font weight, contrast, and optical sizing for dark mode UI in 2026.

· · 12 min read

Updated: October 16, 2026

Dark city street at night lit by glowing neon signs in Osaka, Japan

A dark theme can look flawless in the Figma file and slightly wrong in production. Same font, same weight value, same hex codes checked against a contrast checker. It just looks heavier than it should, almost bold where regular was specified. The problem isn't a bug. It's 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).

Yellow neon lettering glowing at full strength on a black night
Photo by Prateek Katyal on Unsplash

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.

A font weight that looks balanced in light mode does not automatically look balanced in dark mode, and treating them as interchangeable is the most common dark UI mistake.

PropertyLight modeDark mode adjustment
Heading weighte.g. 700Drop one step, e.g. 600
Text colorPure white acceptableOff-white (#E8E8E8-#EDEDED), not pure white
BackgroundPure black acceptableDark gray (#121212-#1A1A1A), not pure black
Line-heightBaseline value10-20% higher
A thin neon tube spelling out one word, shot close in the dark
Photo by Ray T on Unsplash

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. 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 as a rule, because 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.

Photo by Jakub Zerdzicki on Unsplash

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 they hear it. Pure white (#FFFFFF) text on pure black (#000000) technically maximizes your contrast ratio, but extreme polarity like this can tire the eyes and make long stretches of reading harder than a softer pairing would. 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 over a long 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).

Photo by Nat on Unsplash

What Are the WCAG Contrast Standards for Dark Mode Text?

The comfort argument above sits on top of a hard floor: the WCAG numbers your dark theme has to clear before comfort even enters the conversation. Dark mode doesn't get a special exemption. The same ratios apply whether text is dark-on-light or light-on-dark, because a contrast checker measures the pair, not the direction.

Here's the set to check every dark theme against:

ContentWCAG 2.2 AAWCAG 2.2 AAA
Body text (under 24px, or under 18.66px bold)4.5:17:1
Large text (24px+, or 18.66px+ bold)3:14.5:1
UI components and graphics (borders, icons, focus rings)3:1not defined

That last row is the one teams forget. A 1px hairline border or an icon-only button that reads fine in light mode can slip under 3:1 once it sits on #121212, and it fails success criterion 1.4.11 (non-text contrast) even when every paragraph passes. A dark theme can clear every text check and still fail audit on the disabled-state form borders. Embarrassing, and entirely avoidable.

APCA, the perceptual model arriving with WCAG 3, scores this differently. Instead of one ratio it returns an Lc value, and the rough working targets are Lc 75 for body text, Lc 60 for large text, and Lc 45 for non-text elements. APCA rewards exactly the off-white-on-dark-gray pairing the section above argues for, where the old ratio math is blind to polarity and treats #FFFFFF on #000000 as a perfect score. It's still a draft though. Ship against WCAG 2.2 AA today, treat APCA as the direction things are heading, and don't let a checker's green tick talk you out of reading the actual text on the actual background.

Does the Type Scale Itself Need to Change?

The scale mostly holds. The bottom of it doesn't, and that's where dark mode quietly hurts.

Halation, the bloom that makes light text spread into a dark background, is proportional to the stroke, not to the point size. So the same effect that adds a pleasant softness to a 32px heading turns a 12px caption into a smudge. On a light background that caption was merely small. On a dark one it's small and blurry, and the reader's eye does the extra work without knowing why.

Three adjustments handle it, in the order worth trying them.

ProblemFixCost
Captions and labels smudgeraise the smallest step, 12px to 13 or 14pxone token, no layout change
Small text looks bolder than intendeddrop one weight step below 14pxneeds a variable font to do well
The whole ramp feels tightwiden the ratio slightly, 1.200 to 1.250reflows everything, do it early or never

Start with the first one. Raising the floor of the scale is a single token change, and it fixes most of the complaint. Widening the ratio is the expensive move, because every step above it shifts and your layouts move with them; if you're going to do it, do it before the system has consumers, not after.

Worth building the ramp properly rather than picking sizes by eye. Our type scale calculator generates the steps from a base size and ratio, and a sensible starting point for dark surfaces is a 16px base at 1.250, with the smallest step pinned at 14px rather than whatever the ratio produces below the base. The scale is a guide, and the bottom step is where you're allowed to overrule it.

One thing not to do: keep two separate scales, one for light and one for dark. Teams that try it hit the maintenance cost quickly, because every new component then needs two sets of decisions and the two scales drift apart. One scale, with the floor raised for both modes, loses you almost nothing in light mode and saves you from maintaining a fork. The same logic applies to spacing, and our spacing scale generator builds that ramp the same way: one system, tuned for the harder case.

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.

How Do You Wire Dark Mode Typography Into a Theme System?

Everything above ends up as tokens, and the interesting question is which mechanism switches each one. Typography values come in two kinds, and CSS handles them differently.

Colors are the easy kind. The light-dark() function takes two colors and returns the first in a light scheme and the second in a dark one. MDN lists it as Baseline since May 2024, and it needs color-scheme: light dark on the root before it does anything. Numbers are the other kind, and light-dark() can't help: it accepts colors and images, not a font weight or a line height. Those need an override block, either a prefers-color-scheme media query or an attribute on the root.

:root {
  color-scheme: light dark;
  --text-body: light-dark(#1f2937, #e8e8e8);
  --surface: light-dark(#ffffff, #121212);
  --font-weight-heading: 700;
  --line-height-body: 1.5;
}

@media (prefers-color-scheme: dark) {
  :root {
    --font-weight-heading: 600;
    --line-height-body: 1.65;
  }
}

If your product has a manual theme toggle, key the numbers off the attribute and set color-scheme from the same attribute. Otherwise a reader who forces light mode on a dark operating system gets light colors with dark-mode leading, which looks like a bug nobody can reproduce on their own machine.

Name tokens by role, not by value: --text-body, --text-muted, --text-heading. Components read the role and never mention a hex code, which is the same layering a token pipeline gives you, and our guide to modes and brands in Style Dictionary shows how those per-theme files get generated. Then test each role, not each theme. Body text rarely fails. The muted role does, because the common #6b7280 grey clears 4.5:1 on white at 4.83 and lands at 3.88 on #121212, which is exactly the pair the checker above opens on.

One last shortcut to leave alone. -webkit-font-smoothing: antialiased makes light text on a dark background look lighter, and it's tempting as a one-line fix. MDN marks it non-standard and says it only works on macOS, so it does nothing for Windows, Android or Linux readers. Weight and leading tokens work everywhere.

How Do You Test Dark Mode Legibility Before It Ships?

Run three passes on every text role: a numeric contrast check, an automated scan with the dark scheme emulated, and a read-through at the smallest size. Each one catches something the other two miss, and skipping the third is how a theme that "passes" still smudges.

Pass one is the numbers. Check each role against the surface it actually sits on, not just the page background: body, muted, disabled, borders and the focus ring. The thresholds are the ones in the table above, 4.5:1 for body text and 3:1 for large text and non-text elements. The contrast checker embedded earlier on this page takes a pair and gives the ratio. The muted role is where themes fail first, as the 3.88:1 on #121212 earlier shows.

Pass two is a scan. An automated checker measures the colors the page renders, so it has to render the dark theme. In Playwright that's one line before the scan:

await page.emulateMedia({ colorScheme: "dark" });
const results = await new AxeBuilder({ page }).withTags(["wcag2aa"]).analyze();

The scan reads the default rendered state, so hover, focus and disabled styles still need a manual look. A green result means the text it saw passed. It doesn't mean the theme is readable.

Pass three is a person. Switch the theme in Chrome DevTools (Rendering panel, emulate prefers-color-scheme), turn the screen brightness down, and read three paragraphs plus the smallest caption at real size. Does the caption stay crisp? Does the heading feel heavier than the body should allow? A pair can clear 4.5:1 and still smudge at 12px, which is the reason the floor of the scale is worth raising rather than trusting the ratio.

Keep the results with the theme. A dark theme that was tested once is a theme that regresses at the next palette tweak, so run the scan wherever the rest of your accessibility checks already run.

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.

The same type, light panel and dark panel
Edit the CSS

Set the dark panel's --font-weight-heading back to 700 and --line-height-body to 1.5, then compare the two. The heading shouts and the paragraph tightens, and not one value in the markup changed.

Check a pair before you argue about it

It opens on a lighter grey, #9ca3af, over #121212, which scores 7.38:1. Now type #6b7280 into the text field. That grey is a fine secondary text color on white, where it scores 4.83:1, and it drops to 3.88:1 on the dark surface. Slide the text color lighter until the body-text badge flips, then try the off-white from the section above against the same background and read how much room it leaves.

Open the full checker

Large heading text

The quick brown fox jumps over the lazy dog. This is body copy at a normal reading size.

Button label
--contrast ratio
  • AA Normal text 4.5:1
  • AA Large text 3:1
  • AA UI & graphics 3:1

Frequently Asked Questions

Why does the same font weight look bolder in dark mode?
It's an optical effect called irradiation, light text on a dark background visually bleeds outward into the surrounding darkness more than dark text on a light background does. A Medium (500) weight that looks perfectly balanced in light mode can read closer to a Semibold (600) once you flip the background to black. A common over-correction is dropping a heading from 600 to 500 specifically for the dark theme, only for the light theme to then look under-weighted by comparison. The fix is running separate weight tokens per theme, not one weight value shared across both.
Should dark mode body text be pure white on pure black?
No, and this is one of the most common dark mode mistakes. Pure #FFFFFF text on pure #000000 background creates the highest possible contrast ratio, but extreme contrast combinations like this can cause halation and eye strain over long reading sessions compared to softer pairings. Use an off-white, something like #E8E8E8 or #EDEDED, against a dark gray background like #121212 or #1A1A1A rather than true black. You lose almost nothing on the contrast ratio check, WCAG AA only requires 4.5:1, and you gain real reading comfort over long sessions.
What is optical sizing and does it matter for dark mode?
Optical sizing is a font design axis that adjusts letterform proportions (stroke contrast, x-height, spacing) based on the size the type will render at, distinct from just scaling the same shapes up or down. On dark backgrounds specifically, thin strokes in a display typeface can look like they're vibrating or dissolving because of that irradiation effect. Variable fonts with an opsz axis, like Fraunces or Recursive, let you dial in a slightly heavier optical size for dark mode headings without changing the nominal font-weight value. If your type doesn't have an opsz axis, bumping the weight token by one step for dark mode is the practical substitute.
How much should line height increase in dark mode?
Around 10-20% more than your light mode value is a reasonable starting point, though it's worth testing it against your actual typeface rather than applying it blindly. A body font sitting at 1.5 line-height in light mode often reads more comfortably at 1.6-1.65 in dark mode, again because of that visual irradiation, letters feel like they're pressing closer together against a dark field even when the pixel spacing hasn't changed. This is a small, cheap adjustment. Store it as a separate token (--line-height-body-dark) rather than hard-coding it into a single dark-mode override class buried somewhere in your CSS.