How to dark-mode typography: quick-reference checklist
- Step 1Weight — reduce 50 to 100 (automated) — Body: light 400 → dark 300. Already-thin (≤ 300): drop only 50, so 300 → 250, never below 100. Headings bloom more, so the full −100 helps most. [The adjuster](/font-tools/dark-mode-font-adjuster) computes this for you; run it per text role.
- Step 2Letter-spacing — add +0.01em (automated) — A small +0.01em keeps bloomed strokes from fusing. The tool adds exactly this in the dark override. Apply to both body and headings; on tightly-tracked headings it nudges negative tracking one step toward zero, which is the right direction.
- Step 3Line-height — add +0.05 (automated) — Add +0.05 to the unitless line-height (1.5 → 1.55) so glowing rows do not bleed. The tool does this automatically. On very wide measures you may add a touch more by hand; the default is a safe baseline.
- Step 4Text colour — off-white, not pure white (manual) — Use ~#EAEAEA rather than #FFFFFF. Pure white maximises the contrast that causes halation; off-white still clears WCAG with room to spare and reads calmer. The adjuster emits no colour — add this declaration yourself.
- Step 5Background — off-black, not pure black (manual) — Use ~#1C1C1E (Apple's system dark) or #181818 rather than #000000. Pure black intensifies edge contrast and the bloom. Off-black keeps the dark feel while reducing glare. Again, manual — the tool does not output background.
- Step 6Verify on OLED and wire the switcher (manual) — Test on an OLED device where halation is strongest, confirm WCAG AA (4.5:1 body), and if you ship a manual toggle, duplicate the dark rules under `[data-theme="dark"]` so user choice beats the OS. None of these are emitted by the tool.
The full checklist with targets
Every item, its target value, and whether the adjuster does it for you. Run the tool for the 'Yes' rows; handle the 'No' rows by hand.
| Item | Target | Automated by the tool? |
|---|---|---|
| Body weight reduction | Light 400 → dark 300 (−100; −50 at/below 300; min 100) | Yes |
| Heading weight reduction | Light 700 → dark 600 | Yes (run per role) |
| Letter-spacing | +0.01em in dark | Yes |
| Line-height | +0.05 in dark | Yes |
| Text colour | Off-white ~#EAEAEA, not #FFFFFF | No — add by hand |
| Background colour | Off-black ~#1C1C1E / #181818, not #000 | No — add by hand |
| Contrast ratio | WCAG AA 4.5:1 body, easily 12:1+ achievable | No — verify by hand |
| Manual switcher override | [data-theme="dark"] mirrors the media query | No — add by hand |
| OLED verification | Check on an OLED device | No — manual test |
| Same font both modes | Do not swap typefaces | N/A — design rule |
Weight reduction quick table
The dark weight for each light weight, per the tool's rule. Floored at 100; smaller −50 step at or below 300.
| Light weight | Dark weight | Step used |
|---|---|---|
| 100 | 100 | clamped |
| 200 | 150 | −50 |
| 300 | 250 | −50 |
| 400 | 300 | −100 |
| 500 | 400 | −100 |
| 600 | 500 | −100 |
| 700 | 600 | −100 |
| 800 | 700 | −100 |
| 900 | 800 | −100 |
Contrast targets for both modes
WCAG minimums plus the practical recommendation. Note that maxing contrast is not the goal — off-white beats pure white for comfort.
| Mode | Text on background | Ratio | Verdict |
|---|---|---|---|
| Light | #1A1A1A on #FFFFFF | ~16:1 | Comfortable, passes AAA |
| Dark (recommended) | #EAEAEA on #1C1C1E | ~12:1 | Passes AAA, low glare |
| Dark (avoid) | #FFFFFF on #000000 | 21:1 | Max contrast, max halation glare |
| WCAG AA floor | any body text | 4.5:1 | Minimum — both modes clear it easily |
Cookbook
The checklist as runnable CSS. The automated three come straight from the adjuster; the manual items are shown so you can paste a complete, ship-ready dark-mode block.
The automated three (verbatim tool output)
ExampleWhat the adjuster generates for a 400/1.5/0 body. These three lines are the items you do not have to write yourself.
body { font-weight: 400; letter-spacing: 0em; line-height: 1.5; }
@media (prefers-color-scheme: dark) {
body {
font-weight: 300; /* checklist: weight -100 */
letter-spacing: 0.01em; /* checklist: +0.01em */
line-height: 1.55; /* checklist: +0.05 */
}
}The manual colour + background items
ExampleThe off-white-on-off-black layer the tool does not emit. Add it to the same dark media query for a complete block.
@media (prefers-color-scheme: dark) {
body {
color: #EAEAEA; /* checklist: off-white, not #FFF */
background: #1C1C1E; /* checklist: off-black, not #000 */
}
}Headings — run the tool again per role
ExampleHeavier weights bloom more, so headings need the −100 drop too. Generate with your heading values and rename the selector from body.
h1, h2, h3 { font-weight: 700; letter-spacing: -0.01em; line-height: 1.2; }
@media (prefers-color-scheme: dark) {
h1, h2, h3 {
font-weight: 600; /* 700 -> 600 */
letter-spacing: 0em; /* -0.01 + 0.01 */
line-height: 1.25; /* +0.05 */
}
}Manual switcher mirror
ExampleThe data-attribute block that makes a user toggle override the OS preference. The tool emits only the media query; add this to honour an explicit choice.
/* media query: follows the OS */
@media (prefers-color-scheme: dark) {
body { font-weight: 300; letter-spacing: 0.01em; line-height: 1.55; color: #EAEAEA; background: #1C1C1E; }
}
/* data attribute: user toggle wins */
[data-theme="dark"] body {
font-weight: 300; letter-spacing: 0.01em; line-height: 1.55; color: #EAEAEA; background: #1C1C1E;
}Complete ship-ready dark block
ExampleAll checklist items combined into one media query: the automated three plus the manual colour layer. This is the full pass result.
body { font-weight: 400; letter-spacing: 0em; line-height: 1.5; }
@media (prefers-color-scheme: dark) {
body {
font-weight: 300;
letter-spacing: 0.01em;
line-height: 1.55;
color: #EAEAEA;
background: #1C1C1E;
}
}Edge cases and what actually happens
Every row below was probed against the live API. Some documented requirements (alphabetical axis order, numerical tuple order) are not actually enforced in practice — useful to know if you've been blaming the wrong thing for a 400.
Picking pure white text for maximum contrast
Avoid — maximises glare21:1 contrast sounds ideal, but #FFFFFF on #000000 is the worst case for halation — the bloom that makes text look heavy is driven by exactly that maximal contrast. Off-white #EAEAEA on off-black #1C1C1E still passes WCAG AAA (~12:1) and reads far calmer. The adjuster does not set colour, so this choice is yours.
Testing only on an LCD monitor
Incomplete — verify on OLEDBacklit LCDs leak light through black pixels, lowering edge contrast and hiding most of the halation. If you only test on an LCD, the adjustment may look like it does nothing. Always verify on an OLED phone or laptop, where the effect — and your compensation — is fully visible.
Forgetting the manual switcher block
Missing — toggle ignoredThe adjuster emits only a prefers-color-scheme: dark query, which follows the OS. If you ship an in-app dark toggle and forget the [data-theme="dark"] mirror, the toggle will appear to do nothing for the typography. Add the data-attribute block with the same values.
Skipping headings because the tool wrote body
Incomplete — run per roleThe tool writes body each run, so it is easy to adjust body and forget headings. Heavy heading weights bloom the most and benefit most from the −100 drop. Generate a second block with your heading values and rename the selector. Do not leave headings at their light weight.
Thin (100) body cannot be reduced further
Clamped — expected100 − 50 = 50, floored back to 100, so a Thin body stays Thin in dark mode. The tracking and leading deltas still apply. If a Thin face still glares, the remedy is the colour items on the checklist (off-white text), not weight.
Using px letter-spacing instead of em
Avoid — does not scaleThe tool outputs letter-spacing in em on purpose — em scales with font size, so the +0.01em is proportional at every size. A fixed px tracking would be too much at small sizes and too little at large. Keep the em unit the tool emits.
Overriding font-size or font-family in the dark block
Unnecessary — keep constantOnly weight, spacing, line-height, and colour should change between modes. Resizing or re-fonting in dark mode is jarring during the OS transition and is not on this checklist. The adjuster correctly leaves size and family alone.
Browser lacks prefers-color-scheme support
Supported — falls back to lightOlder or non-supporting browsers ignore the media query and use the light baseline. Every checklist item lives inside the dark media query (or its data-attribute mirror), so there is no breakage — the page simply renders in light mode.
Frequently asked questions
Which checklist items does the tool automate?
Three: the weight reduction (−100, or −50 at/below 300, floored at 100), the +0.01em letter-spacing, and the +0.05 line-height. Run the adjuster for those. The colour, background, contrast verification, manual-switcher block, and OLED testing are all yours to add.
What contrast ratio should I target in dark mode?
WCAG AA needs 4.5:1 for body text, which dark mode clears easily. The recommendation is off-white text (~#EAEAEA) on off-black (~#1C1C1E) at around 12:1 — comfortably AAA without the glare of pure white on pure black at 21:1. Maxing contrast is not the goal; comfort is.
Why off-white instead of pure white text?
Pure white on pure black is the maximum-contrast case that drives halation — the bloom that makes text look heavy. Off-white at ~#EAEAEA cuts the glare at its source while still passing WCAG with room to spare. The adjuster does not set colour, so make this choice yourself.
What background colour should dark mode use?
Off-black: ~#1C1C1E (Apple's system dark background) or #181818, not #000000. Pure black intensifies the edge contrast that causes halation. Off-black keeps the dark-mode feel while reducing the bloom. This is a manual addition — the tool emits no background.
How much should I reduce the weight?
Body 400 → 300, headings 700 → 600 — a flat −100 above weight 300. At or below 300 the step is −50 (300 → 250, 200 → 150), and it never drops below 100 (Thin stays Thin). The adjuster applies this automatically; run it per text role.
Should I use a different font in dark mode?
No. Same font, adjusted weight/spacing/line-height. A different typeface is jarring during the OS theme transition and doubles font bandwidth. The checklist explicitly keeps the font constant — the tool only changes the three typographic deltas.
Do I need to do anything for headings separately?
Yes. The tool writes body each run, and headings bloom more than body text because heavier weights have more lit area. Generate a second block with your heading weight/tracking/leading and rename the selector. Body and headings should both get the dark treatment.
How do I handle a manual theme toggle?
The tool's output follows the OS via prefers-color-scheme. If you ship a toggle, duplicate the dark rules under [data-theme="dark"] so an explicit choice overrides the system preference. Keep both blocks in sync. The tool does not emit the data-attribute variant.
Why test specifically on OLED?
OLED panels switch black pixels fully off, producing maximal contrast at stroke edges — which is what drives halation. LCDs leak backlight and hide most of it. Testing only on LCD can make the adjustment look like it does nothing. Verify on an OLED device for the true result.
What unit is the letter-spacing in?
em. The tool outputs letter-spacing in em so the +0.01em scales with font size and stays proportional at every size. Avoid converting it to px — fixed px tracking is wrong at small or large sizes.
Does the dark block break browsers without dark mode?
No. Everything sits inside the prefers-color-scheme: dark media query (or its data-attribute mirror), so unsupporting browsers ignore it and render the light baseline. The output is additive and safe to ship globally.
How does this checklist relate to font loading and scale?
This list covers dark-mode rendering only. For the wider type system, pair it with typography-scale-builder for sizes, font-display-strategy for load behaviour, and css-variable-generator-font for the token layer. They are complementary, not overlapping.
Privacy first
Every JAD Font tool runs entirely in your browser using opentype.js and the wawoff2 WASM Brotli encoder. Your fonts never leave your device — verified by zero outbound network requests during processing.