How to encode dark-mode typography adjustments as design tokens
- Step 1Get the value pairs from the tool — Run [the adjuster](/font-tools/dark-mode-font-adjuster) with your light body values. Note both the light values you typed and the three dark values it emits — those six numbers are everything you need. For weight 400 / lh 1.5 / ls 0 you get dark 300 / 1.55 / 0.01em.
- Step 2Define the light token set — Put the light values in your base token file. Example: `"font-weight-body": { "$value": 400, "$type": "fontWeight" }`, `"line-height-body": { "$value": 1.5 }`, `"letter-spacing-body": { "$value": "0em" }`. These are the defaults all components see.
- Step 3Define the dark override set — In a dark-override token file, include only the keys that differ: `"font-weight-body": { "$value": 300 }`, `"line-height-body": { "$value": 1.55 }`, `"letter-spacing-body": { "$value": "0.01em" }`. Font-family, sizes, and the rest stay in the base set untouched.
- Step 4Build to a CSS variable + media query — Configure your token build (Style Dictionary or similar) to emit `:root { --font-weight-body: 400; … }` and `@media (prefers-color-scheme: dark) { :root { --font-weight-body: 300; … } }`. This is the same structure the tool emits, just driven by tokens.
- Step 5Reference the variables in components — Components use `font-weight: var(--font-weight-body)` and never branch on mode. When the OS flips to dark, the media query rebinds the variable and every consumer updates. No component-level dark logic.
- Step 6Add a manual-override hook if you ship a switcher — The tool's output, and the media-query token build, key off the OS preference only. If you have a theme toggle, also emit `[data-theme="dark"] :root { … }` with the same dark values so user choice beats system preference. Keep both blocks in sync with the tool's numbers.
What the tool emits vs. what a token build needs
The adjuster gives you resolved CSS, not tokens. This is the mapping you perform by hand to bridge the two.
| Tool output | Token-build equivalent | Who writes it |
|---|---|---|
body { font-weight: 400; … } | :root { --font-weight-body: 400; … } | You, transcribing |
@media (prefers-color-scheme: dark) { body { font-weight: 300; … } } | @media (prefers-color-scheme: dark) { :root { --font-weight-body: 300; … } } | You, transcribing |
| Resolved numbers (300, 1.55, 0.01em) | Token $values in two files | You, transcribing |
Selector body | Selector :root (variables) | Token build config |
| Style Dictionary JSON | — | Not emitted — tool gives CSS only |
Which token keys change between modes
Only the three properties the adjuster touches need a dark override. Everything else stays in the base token set — overriding more is wasted churn.
| Token | Light $value | Dark override? | Dark $value |
|---|---|---|---|
font-weight-body | 400 | Yes | 300 |
letter-spacing-body | 0em | Yes | 0.01em |
line-height-body | 1.5 | Yes | 1.55 |
font-family-body | Inter, … | No | — |
font-size-body | 1rem | No | — |
color-text | #1A1A1A | Yes (but tool does not emit it) | #EAEAEA — add by hand |
Value pairs for common body weights
Pre-computed light/dark token values so you can seed your files without re-running the tool for each. Letter-spacing and line-height deltas are fixed (+0.01em, +0.05).
| Light weight | Dark weight token | Light lh → dark lh | Light ls → dark ls |
|---|---|---|---|
| 400 | 300 | 1.5 → 1.55 | 0em → 0.01em |
| 500 | 400 | 1.5 → 1.55 | 0em → 0.01em |
| 600 | 500 | 1.4 → 1.45 | 0em → 0.01em |
| 700 | 600 | 1.2 → 1.25 | -0.01em → 0em |
| 300 | 250 | 1.6 → 1.65 | 0em → 0.01em |
Cookbook
Transcription recipes from the tool's CSS output into token files and back to a media-query build. The numbers all come from the adjuster; the token shapes are W3C DTCG / Style Dictionary style.
Light token file (base)
ExampleThe light-mode values you typed into the tool, expressed as DTCG tokens. These are the defaults; the dark file overrides only what changes.
// tokens/typography.json
{
"font-weight-body": { "$value": 400, "$type": "fontWeight" },
"line-height-body": { "$value": 1.5 },
"letter-spacing-body":{ "$value": "0em" }
}Dark override file
ExampleOnly the three keys the adjuster changes, with the exact values it computed for a 400/1.5/0 baseline. Everything else inherits from the base file.
// tokens/typography.dark.json
{
"font-weight-body": { "$value": 300 },
"line-height-body": { "$value": 1.55 },
"letter-spacing-body":{ "$value": "0.01em" }
}Built CSS — matches the tool's structure
ExampleWhat your token build should emit. Structurally identical to the adjuster's output, but driven by variables so components consume one source.
:root {
--font-weight-body: 400;
--line-height-body: 1.5;
--letter-spacing-body: 0em;
}
@media (prefers-color-scheme: dark) {
:root {
--font-weight-body: 300;
--line-height-body: 1.55;
--letter-spacing-body: 0.01em;
}
}
body {
font-weight: var(--font-weight-body);
line-height: var(--line-height-body);
letter-spacing: var(--letter-spacing-body);
}Add a manual switcher override
ExampleThe media query follows the OS. If you ship a toggle, duplicate the dark values under a data attribute so the user's explicit choice wins over the system setting.
@media (prefers-color-scheme: dark) {
:root { --font-weight-body: 300; --line-height-body: 1.55; --letter-spacing-body: 0.01em; }
}
/* manual override beats system preference */
[data-theme="dark"] :root,
:root[data-theme="dark"] {
--font-weight-body: 300; --line-height-body: 1.55; --letter-spacing-body: 0.01em;
}Regenerate values headless in CI
ExampleBecause the tool is generative with no upload, you can fetch the schema and POST the three options to the local runner, then transcribe the returned CSS into your token files in a script. Deterministic, so it never produces a spurious diff.
# schema
curl http://127.0.0.1:9789/v1/tools/dark-mode-font-adjuster/run \
-H 'content-type: application/json' \
-d '{"options":{"lightWeightClass":400,"lightLineHeight":1.5,"lightLetterSpacingEm":0}}'
# -> CSS with dark values 300 / 1.55 / 0.01em; parse and write to tokens.dark.jsonEdge 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.
Expecting the tool to output Style Dictionary JSON
Not emitted — CSS onlyThe adjuster's output type is css: a body {} rule plus a prefers-color-scheme: dark override. It does not emit DTCG/Style Dictionary tokens. Use it to compute the value pairs, then hand-author the token files as shown above. The values are deterministic, so this is a one-time transcription per body role.
Tool writes body, your tokens build to :root
Reconcile selectors by handThe tool always targets body; a variable-based token build targets :root and you apply the variables on body. When transcribing, move the resolved numbers into :root custom properties and keep the var() references on body. The numbers are identical; only the selector strategy differs.
Overriding font-family or size per mode
Unnecessary — wasted churnOnly weight, letter-spacing, and line-height should differ between modes — that is all the adjuster changes and all most designs need. Putting font-family or font-size in the dark override file adds diff noise and risks the two modes drifting apart. Keep non-changing tokens in the base file only.
Colour token has no source value from the tool
Out of scope — author separatelyHalation is also a colour problem (off-white text on off-black), but the adjuster emits no color. Your color-text dark override (~#EAEAEA on ~#1C1C1E) must be authored independently. It belongs in the same dark token file for cohesion, but the value does not come from this tool.
Manual theme switcher ignored by a media-query-only build
Add a data-attribute blockA pure prefers-color-scheme build follows the OS and ignores in-app toggles. If you ship a switcher, emit a [data-theme="dark"] block with the same dark values so user choice overrides the system. The tool only emits the media-query form, so you add the data-attribute variant in your build config.
Non-step weight produces a non-step dark token
Accepted — but snap to real weightsThe handler does plain subtraction, so a light value of 450 yields a dark token of 350. Browsers snap numeric font-weights to the weights a static font actually contains, so 350 may render as 400 anyway. Seed tokens with weights your font file ships to keep the rendered result predictable.
Line-height dark value exceeds the input maximum
Valid — just cannot be re-fedA light line-height of 2.0 yields a dark token of 2.05, which is a perfectly valid unitless value but is above the tool's 2.0 input cap. Store 2.05 in your dark token; just do not try to round-trip 2.05 back through the tool as a light input.
Cross-platform tokens (iOS / Android) need separate transforms
Supported by your build, not the toolStyle Dictionary can emit per-platform mode handling — UIUserInterfaceStyle for iOS, night-* resource folders for Android — from the same source tokens. The adjuster only gives you web CSS numbers; your token pipeline is responsible for the platform transforms. The light/dark $value pairs are the shared source of truth.
Frequently asked questions
Does the adjuster output design tokens?
No. Its output is CSS — a body {} rule and a @media (prefers-color-scheme: dark) override. To use it for tokens, run it to compute the exact light/dark value pairs, then transcribe those numbers into your token files. The deltas are deterministic, so it is a one-time transcription per text role.
Which tokens actually need a dark override?
Only the three the tool changes: font-weight, letter-spacing, and line-height. Font-family, font-size, and most other typography stay constant across modes — keep them in your base token file and override only what differs to avoid diff churn.
How do I map body to :root for a variable build?
Put the resolved numbers in :root custom properties (--font-weight-body: 400), add a @media (prefers-color-scheme: dark) { :root { --font-weight-body: 300 } } block, and reference var(--font-weight-body) on body. The values come straight from the tool; you just change the selector strategy from body to :root.
Will the values change between runs?
No. The tool is fully deterministic — the same three inputs always yield the same three outputs (weight − 100/− 50 floored at 100, +0.01em, +0.05). That makes the generated values safe to commit; your token files will not churn between regenerations.
Can I regenerate the values in CI?
Yes. The tool is generative with no font upload, so you can POST the three options to the local runner at 127.0.0.1:9789/v1/tools/dark-mode-font-adjuster/run, parse the returned CSS, and write the dark values into your token file. Because it is deterministic, the diff is empty unless your light inputs changed.
How do I handle a manual theme switcher?
The tool and a media-query token build both follow the OS preference only. Add a [data-theme="dark"] :root { … } block with the same dark values so an explicit user choice beats the system setting. Keep that block in sync with the tool's numbers.
Where do colour tokens come from?
Not from this tool. The adjuster emits only weight, spacing, and line-height. Your dark color-text (around #EAEAEA) and background (around #1C1C1E) are authored separately — they belong in the same dark token file for cohesion but the values are yours to choose. The checklist has recommendations.
Should I override every property per mode?
No — most properties do not change. Only weight, letter-spacing, line-height, and colour differ between light and dark. Overriding font-family or sizes is wasted churn and risks the modes drifting. Override the minimum.
Can I theme cross-platform from the same tokens?
Yes, but that is your token build's job, not the tool's. Style Dictionary's mode-aware transforms emit media queries for web, UIUserInterfaceStyle for iOS, and night-* resources for Android from one source. The adjuster supplies the web numbers; the light/dark value pairs are the shared source.
What about variable-font weight axes in tokens?
The adjuster emits a static font-weight number. If your tokens drive a variable wght axis, take that number and store it as the axis value, or generate axis CSS with css-variable-generator-font. The dark weight (e.g. 300 from a light 400) is a valid axis value either way.
Is the output safe to commit to version control?
Yes. The CSS and the transcribed token values are deterministic and contain no machine-specific data — no font bytes, no timestamps in the values. Commit them like any other source. Regenerating yields byte-identical numbers unless your light inputs change.
How does this relate to font-face and scale tokens?
This tool covers only the dark-mode deltas for weight/spacing/leading. Pair it with css-variable-generator-font for the broader type token set, typography-scale-builder for the size scale, and font-face-generator for the @font-face declarations. Together they form a complete typography token layer.
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.