Blog · September 19, 2026

Setting white type on dark without the bloom.

Take a headline that looks perfect on a light frame, drop it unchanged onto a dark one, and it comes back wrong: heavier, slightly furry at the edges, the counters filled in. Same font, same weight, same pixel size. Nothing in the file changed. What changed is that light on dark and dark on light are not the same optical problem, and type set for one never quite works in the other.

Why the letters get fatter

Bright shapes on dark ground appear larger than dark shapes of identical size on light ground. Helmholtz described this — irradiation — in the nineteenth century, and you can reproduce it in ten seconds: set a white square on black next to a black square on white at the same dimensions and look. The white one wins. Light scatters inside the eye, and that scatter spills across the boundary into the dark side, eating a sliver of the surrounding space.

For a solid square that just reads as "slightly bigger." For letterforms it reads as heavier, because a typeface is mostly boundary. Every stem grows a fraction on both sides, so a stem that was 4px of ink now reads as 5. The gaps shrink from both sides at once, so the hole in an e or an a closes faster than the stroke thickens. The net effect is a font that behaves like it moved half a weight up the scale — and at display sizes on a bright OLED at full brightness, it gets worse, not better.

This is why a carousel with three light frames and two dark ones can look sloppy even when every frame uses the same type styles. The type styles being identical is the bug.

1. Drop a weight on the dark frames

The simplest correction, and the one that does most of the work: if the light frames are Semibold, set the dark ones Medium. If light is Bold, dark is Semibold. You are not making the dark headline lighter — you are making it match, because the eye is about to add back what you took away.

With a variable font this is a dial rather than a jump: Inter, Figtree, Outfit and the rest of the modern variable families take any weight value, so a 600 headline on white becomes roughly 540–560 on black. Set both versions side by side at the size they'll actually be seen and pick the value where they stop looking like different fonts.

The same logic runs the other way at the bottom of the scale. A Light or Thin weight that looks refined on white goes brittle on black, where scatter attacks thin strokes the hardest — smeary rather than delicate. Below about 18px on a dark ground, Regular is the floor.

2. Stop using #FFFFFF

Pure white on pure black is 21:1 contrast — the absolute maximum the sRGB space can produce, and far past the point where more helps. What you get past roughly 15:1 is halation: the glow that makes text feel like it's vibrating slightly off the background. It's worse for readers with astigmatism, worse in a dark room, and worse on an OLED panel where the background is genuinely off.

Back the ink off a few percent and the glow goes with it. A warm off-white like #EDEAE4 on a near-black #0B0B0D still measures 16.4:1 — more than three times the 4.5:1 that WCAG asks for body text, so you are spending nothing you needed. It also gives your palette somewhere to go: pure white has no brighter state, so a genuine highlight has nowhere left to land.

The reference numbers, computed rather than guessed: #FFFFFF on #000000 = 21.0:1. #EDEAE4 on #0B0B0D = 16.4:1. #E8E4DC on #0B0B0D = 15.5:1. All three pass every accessibility threshold by a wide margin. Only the first one glows.

3. Lift the background off zero too

The same argument applies at the other end. A true #000000 background costs you the shadows: nothing can sit behind anything, because there is no darker value to recede into, so cards, device frames and drop shadows all flatten onto one plane. Start the darkest value around #0B0B0D and the whole frame gains depth for free.

A dark background is also where 8-bit gradients band worst, because the steps between adjacent dark values are perceptually enormous — that's a separate problem with its own fixes, covered in the stripes in your gradient.

4. Open the tracking a hair

If irradiation eats the space between letters, give some back. A touch of positive tracking on dark headlines — on the order of half a percent to one percent of the font size — restores the rhythm the scatter took. It's a perceptual correction, so the number that matters is the one that looks right at final size.

Note this runs against the usual advice that big type wants tighter tracking. Both are true: display sizes want negative tracking, dark backgrounds want less negative tracking than you'd use on white. A 96px white headline on black might land at −1% where the same headline on cream sits at −2%.

5. Use the display cut at display sizes

Serious type families ship optical sizes: a text cut with open spacing and sturdier strokes for small sizes, a display cut with tighter spacing and finer detail for large ones. Apple's San Francisco does this automatically inside its own UI — the design transitions from the text design to the display design somewhere between roughly 17 and 28 points, and the system also adjusts tracking across that range. You get it for free on an iOS screen and not at all in an exported PNG.

In a design tool you have to ask. If the family exposes an opsz axis (Inter, Fraunces, Newsreader and a growing list do), set it to the actual rendered size rather than leaving it at the default. If it ships as separate Text and Display files, use Display above ~24px. On a dark background this matters more than usual, because the display cut's finer strokes are the ones irradiation is about to thicken back toward normal.

How to check it in thirty seconds

Three tests, all faster than arguing about it:

The squint test. Half-close your eyes at the full set. Squinting approximates the blur that bloom adds, so whichever frame goes to mush first is the one that's over-inked.

The thumbnail test. Shrink the set to the size it appears in store search results and look for closed counters. Letters whose holes fill in are the giveaway that the weight is a step too high for the background.

The invert test. Flip the frame's colours — dark type on light — and compare to your light frames. If the inverted version suddenly looks too thin, your dark version is compensated correctly. If it looks identical, you didn't compensate at all.

And check on a phone at the brightness you actually use, at night. Bloom is a function of luminance; a dark frame reviewed on a dimmed laptop in a bright room is the one condition where the problem hides.

Why this shows up in screenshots more than in apps

An app renders its type live, so the OS can apply optical sizing, hinting and system tracking on the user's device. A store screenshot is a flat PNG that was rasterised once, on your machine, and then shrunk into a search result by a device that has no idea it contains text. Every optical decision is frozen at export. If the weight was a step too heavy on a dark frame, it stays a step too heavy in every slot, on every device, until you re-export.

Which is the practical reason to care: dark backgrounds are the default choice for "premium" screenshot sets, and they are the hardest surface to set type on — but the fix is five adjustments applied once, to a template you reuse.

ShotCanvas's dark templates already carry the compensation — off-white ink rather than #FFFFFF, backgrounds lifted off true black, weight and tracking tuned per background rather than shared across the set — so a headline that reads cleanly on a light template still reads cleanly when you switch. And every export is pixel-exact for both stores.

Design a dark set free Next: readable text over any image