How to adopt system stack as design system default for body
- Step 1Generate the canonical body token value — Set Style = Sans-serif and OS = All, then **Generate**. The emitted `--font-system-sans` value is the canonical body stack to pin. For a public, multi-OS product, All is the right scope — never scope the design-system default to one OS.
- Step 2Generate the code and prose tokens too — Run again with Style = Monospace (for a `font-mono` token) and, if you support long-form content, Style = Serif (for a `font-serif` token). Three runs give you the full set of native typography tokens.
- Step 3Transcribe the values into your token source of truth — Copy each value into Style Dictionary, Tailwind `theme.fontFamily`, CSS-vars, or wherever your tokens live. The tool outputs CSS — there's no token-file export — so this is a one-time transcription per token, then the design system owns it.
- Step 4Write the policy: system by default, custom by exception — Document that body, UI, and code default to the system tokens. Custom web fonts are permitted only on an explicit allow-list of surfaces — typically display headings, marketing pages, and the wordmark — each with a named owner and a byte budget.
- Step 5Add a lint/review gate against re-derived stacks — Make it a review rule (or a lint check) that component CSS must reference the token, not a raw `-apple-system, …` literal. This prevents the canonical stack from drifting as teams copy-paste their own variants.
- Step 6Version and changelog the token, not the components — If you ever update the canonical stack (e.g. add `ui-sans-serif` to the front), change it in one place and changelog it. Because every component references the token, the update propagates without touching component code.
The default tokens to pin
Generate each with the listed Style + OS, then transcribe the value into your token source. These are the canonical defaults for a multi-OS product.
| Token | Generate with | Surface it governs | Default? |
|---|---|---|---|
font-sans / body | Style = Sans-serif, OS = All | Body, UI, controls | Yes — the design-system default |
font-mono / code | Style = Monospace, OS = All | Code blocks, tabular figures | Yes — for any code surface |
font-serif / prose | Style = Serif, OS = All | Long-form articles (optional) | Optional — only if you publish prose |
font-display / brand | Not this tool | Headlines, hero, wordmark | No — explicit opt-in web font |
System-default vs custom-font policy
The governance line. The default path is free and fast; the exception path is allow-listed and budgeted.
| Policy dimension | System-default (the norm) | Custom-font exception |
|---|---|---|
| Who can use it | Every team, every screen | Allow-listed surfaces only |
| Approval | None needed (it's the default) | Design-system owner sign-off |
| Byte budget | 0 KB | Named budget per surface (e.g. ≤40 KB) |
| Loading strategy | None | font-display + preload decision required |
| Privacy review | Not needed (no CDN call) | Self-host required; no third-party CDN |
| Token reference | Must use var(--font-sans) etc. | Must include system token as fallback |
Migration from a cargo-culted stack
Common pre-policy states and how to converge them on the canonical token.
| Current state | Problem | Action |
|---|---|---|
Each component has its own -apple-system, … | Drift — they diverge over time | Replace all literals with the canonical token; lint to enforce |
| Outdated chain missing Roboto/Linux fonts | Poor rendering on Android/Linux | Regenerate with OS = All; pin the current full chain |
| Web font used for body 'because brand' | 150 KB on every screen, CLS | Move body to system token; keep brand font on display only |
Mono hand-typed, missing ui-monospace | Suboptimal on Apple | Regenerate Monospace / All; pin the value |
Cookbook
Policy-ready recipes: the canonical token values to pin, how to wire them in common token systems, and how to encode the exception path.
The canonical body + code tokens (pin these)
ExampleGenerate once each and pin the values. This is the typography foundation most product design systems standardise on for a multi-OS audience.
/* :root tokens to pin (from this generator, OS = All) */
:root {
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
body { font-family: var(--font-sans); }
code, pre { font-family: var(--font-mono); }Tailwind theme transcription
ExampleTailwind has no system-stack importer — you transcribe the generated value into theme.fontFamily once. After that, font-sans and font-mono utilities resolve to the canonical stack across the whole codebase.
// tailwind.config — value copied from the generator output
export default {
theme: {
fontFamily: {
sans: ['-apple-system','BlinkMacSystemFont','"Segoe UI"','Roboto','Oxygen','Ubuntu','Cantarell','"Helvetica Neue"','Arial','sans-serif'],
mono: ['ui-monospace','SFMono-Regular','"SF Mono"','Menlo','Monaco','Consolas','"Liberation Mono"','"Courier New"','monospace'],
},
},
};The exception: a budgeted brand display font
ExamplePolicy permits a custom font on display surfaces only, and it must list the system token as fallback so it degrades to the design-system default while loading.
/* Allow-listed surface: marketing hero only */
@font-face {
font-family: "Brand Display";
src: url(/fonts/brand-display.woff2) format("woff2"); /* self-hosted, ≤40 KB */
font-display: swap;
}
.hero-title {
font-family: "Brand Display", var(--font-sans); /* falls back to the system default */
}Form controls inherit the token
ExampleThe generator wires only body. In a design system, explicitly make form controls inherit so they don't drop to the UA default — a common gap teams hit after adopting the policy.
/* Add to your base/reset layer once */
input, textarea, select, button, optgroup {
font-family: inherit; /* picks up var(--font-sans) from body */
}Lint rule to prevent re-derived stacks
ExampleEncode the policy so component CSS can't hard-code its own -apple-system chain. The exact mechanism depends on your stylelint/eslint setup; the intent is to ban the raw literal outside the token definition.
/* Policy intent (pseudo-rule): DISALLOW the literal substring "-apple-system" anywhere EXCEPT in tokens.css where --font-sans is defined. Components must reference var(--font-sans) instead. */ // stylelint plugin / regex review gate flags: // font-family: -apple-system, ... ← reject in component files // font-family: var(--font-sans); ← allowed
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.
Expecting a Style Dictionary / JSON token export
By designThe generator outputs CSS — a :root variable and a body rule — not a design-token file. You transcribe the value into your token source once. For an automated, multi-format pipeline (CSS + Swift + Kotlin tokens) driven from the same stacks, see Generate System Font Stacks Programmatically.
Scoping the design-system default to one OS
Anti-patternNever pin a single-OS scope (e.g. windows) as the default for a public product — it strips the fonts other platforms need. Use OS = All for the canonical token. Single-OS scopes are only appropriate for a platform-locked surface (an internal Windows-only tool, a native WebView).
Teams re-deriving their own stack in component CSS
Drift riskThe biggest governance failure is each component hard-coding its own -apple-system, … literal. Over time they diverge and the 'canonical' stack is fiction. Enforce a token reference (var(--font-sans)) and lint against raw literals outside the token file.
Form controls don't inherit the token
Common gapBecause the generator wires only body, <input>/<select>/<button> may render in the UA default font. Add input, textarea, select, button { font-family: inherit; } to your reset so the design-system default actually reaches every control.
Brand font used for body 'because brand'
Budget breachLetting a custom font become the body default reintroduces the 150 KB / CLS / privacy costs the policy exists to avoid. Keep body on the system token; the brand font stays on the allow-listed display surfaces with a named byte budget.
Serif default differs a lot across OSes
Trade-offThe system serif token renders as Iowan Old Style (Apple), Cambria (Windows), or Roboto Slab (Android) — more variation than the sans tokens. If your prose brand needs consistency, the policy can carve out a single self-hosted serif as the prose exception rather than using the system serif token.
Updating the canonical stack later
Versioned changeIf you decide to prepend ui-sans-serif or add a newer fallback, change the token in one place and changelog it. Because components reference the token, not a literal, the update is a one-line change that propagates everywhere — the whole point of pinning a token.
Serif / All literal has unquoted multi-word names
QuirkIf you pin the serif / All value as-is, note its first two entries (Iowan Old Style, Apple Garamond) are unquoted despite spaces. For a strictly-valid token, quote them when transcribing, or pin the macOS-scoped serif value which already quotes them.
Frequently asked questions
Why make a system stack the design-system body default?
Because the body token is what most screens use, and defaulting it to a system stack means every screen paints instantly, no team accidentally ships a heavy font family, and there's no third-party font CDN in the default path. Teams must consciously opt into a web font, not slip one in by default.
Which value should I pin for the body token?
Generate Style = Sans-serif, OS = All, and pin that --font-system-sans value. For a public multi-OS product, All is correct — it resolves to San Francisco, Segoe UI, or Roboto per platform. Never pin a single-OS scope as the default.
Does the generator export a design-token file?
No — it outputs CSS (a :root variable plus a body rule). You transcribe the value into your token source (Style Dictionary, Tailwind, CSS vars) once. For an automated multi-format token pipeline, see the programmatic generation guide.
How do I keep teams from writing their own font stacks?
Define the stack once as a token and lint against raw -apple-system literals in component CSS. Components must reference var(--font-sans). The lint/review gate is what actually prevents drift — the token alone doesn't enforce itself.
What about code and prose — separate tokens?
Yes. Generate Monospace / All for a font-mono token and, if you publish long-form content, Serif / All for a font-serif token. Three runs give you a complete native typography foundation: sans for UI, mono for code, serif for prose.
When is a custom font allowed under the policy?
On an explicit allow-list — typically display headings, marketing pages, and the wordmark. Each exception needs an owner, a byte budget, self-hosting (no third-party CDN), and must list the system token as its fallback so it degrades to the default.
Do form controls pick up the default automatically?
Not reliably — the generator wires only body, and many browsers don't inherit font-family on <input>/<select>/<button>. Add input, textarea, select, button { font-family: inherit; } to your reset layer so the design-system default reaches every control.
How do I update the canonical stack later?
Change the token value in one place and changelog it. Because every component references the token rather than a literal, the change propagates without touching component code. That single-source-of-truth property is the main reason to pin a token.
What's the migration path from a stale stack?
Regenerate with OS = All to get the current full chain (with Roboto/Linux fonts and ui-monospace), pin it as the token, then replace every component's hard-coded literal with a token reference. Add the lint gate so the old literals can't creep back.
Should the serif token be a system stack or a web font?
Depends on how much prose consistency matters. The system serif varies a lot across OSes (Iowan vs Cambria vs Roboto Slab). If that's acceptable, use the system serif token; if your prose brand needs one look everywhere, carve out a single self-hosted serif as the prose exception.
Is a system stack compatible with accessibility requirements?
Yes. System fonts are highly legible and present on every device, and they never leave text invisible during a load window. Pair the family token with rem-based sizes (see the clamp generator) so user font-size preferences are respected.
Can the whole policy be automated into the build?
The token transcription and a CI lint gate can. For generating the stack values themselves in CI (so a build can assert the token matches the canonical stack), the programmatic generation guide shows the runner API and source-data approaches.
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.