How to build a typography token system for your design system
- Step 1Generate the raw scale — Set Family name, Base size (10–32px), Ratio, and Steps to match your foundation. For a typical product system, base 16px, Major Third (1.25), and 7–9 steps gives a usable `xs`→`4xl`/`5xl` ladder. Copy the `:root` block — this is your value layer.
- Step 2Save it as tokens-base.css — Put the generated block in a dedicated file imported first. This is the only file that changes when your foundation changes (new base size, new ratio). Components never reference it directly — they go through the semantic layer.
- Step 3Author the semantic layer by hand — In a second `:root` block (or the same file, below the generated one), alias roles to scale steps: `--font-display: var(--font-size-3xl); --font-heading: var(--font-size-2xl); --font-body: var(--font-size-base); --font-caption: var(--font-size-sm); --font-code: var(--font-size-sm)`. The generator does not produce these — there is no role-token option.
- Step 4Bind weights and line-heights into roles — Compose, don't just point: `--font-heading-weight: var(--font-weight-bold); --font-heading-leading: var(--line-height-tight)`. Now a component references `var(--font-heading)` and its companions, never a raw scale step.
- Step 5Wire components to roles only — `h1 { font-size: var(--font-display); font-weight: var(--font-heading-weight); }`. A redesign re-binds the role once; every component follows. This is why the two-layer split matters.
- Step 6Version and document the foundation inputs — Record the four generator inputs (family, base, ratio, steps) in your design-system docs. Regenerating with the same inputs reproduces the exact same `:root` block byte-for-byte, so the value layer is deterministic across rebuilds.
Two-layer token architecture
Which layer this tool generates and which you author. The generator owns the value layer; the semantic layer is intentionally left to your team.
| Layer | Example tokens | Who creates it |
|---|---|---|
| Value (raw scale) | --font-size-base, --font-size-3xl, --font-weight-bold, --line-height-tight | This generator (the :root block) |
| Semantic (roles) | --font-display: var(--font-size-3xl), --font-body: var(--font-size-base) | You, by hand (no option for this) |
| Component | h1 { font-size: var(--font-display) } | Your component CSS |
Suggested role-to-step aliases (default 16px / Major Third)
A sane starting map from semantic roles to the generated scale steps. You write these — the tool does not. Adjust to your component library's roles.
| Semantic role | Alias to | Resolves to |
|---|---|---|
--font-display | var(--font-size-3xl) | 2.441rem |
--font-heading | var(--font-size-2xl) | 1.953rem |
--font-subheading | var(--font-size-xl) | 1.563rem |
--font-body-large | var(--font-size-lg) | 1.250rem |
--font-body | var(--font-size-base) | 1.000rem |
--font-caption | var(--font-size-sm) | 0.800rem |
--font-code | var(--font-size-sm) | 0.800rem |
What the generator gives a design system out of the box
The fixed token groups every output includes — your system inherits a full vocabulary, not just sizes.
| Group | Count | Range |
|---|---|---|
| Size steps | 3–12 (your choice) | Tailwind labels xs→8xl, in rem |
| Weights | 6 | 300 → 800 |
| Line heights | 5 | 1.2 → 2 |
| Letter spacings | 5 | -0.05em → 0.05em |
| Family tokens | 2 | base (your family) + mono (fixed stack) |
Cookbook
The two-layer pattern in practice. Block one is generated; block two is what your team writes on top of it.
Generated value layer (trimmed)
ExampleOutput for base 16px, Major Third, 9 steps. The semantic layer is NOT here — the tool stops at the scale.
/* tokens-base.css — generated */
:root {
--font-family-base: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-weight-regular: 400;
--font-weight-bold: 700;
--line-height-tight: 1.2;
--line-height-normal: 1.5;
--font-size-xs: 0.640rem;
--font-size-sm: 0.800rem;
--font-size-base: 1.000rem;
--font-size-lg: 1.250rem;
--font-size-xl: 1.563rem;
--font-size-2xl: 1.953rem;
--font-size-3xl: 2.441rem;
--font-size-4xl: 3.052rem;
--font-size-5xl: 3.815rem;
}Hand-authored semantic layer
ExampleYou write this file. Roles alias scale steps and compose weight/leading. None of it comes from the generator.
/* tokens-semantic.css — yours */
:root {
--font-display: var(--font-size-4xl);
--font-display-weight: var(--font-weight-extrabold);
--font-display-leading: var(--line-height-tight);
--font-heading: var(--font-size-2xl);
--font-heading-weight: var(--font-weight-bold);
--font-body: var(--font-size-base);
--font-body-leading: var(--line-height-normal);
--font-caption: var(--font-size-sm);
--font-code: var(--font-size-sm);
}Components reference roles only
ExampleThe payoff: components never touch raw steps. A redesign re-binds the role once.
.hero h1 {
font-size: var(--font-display);
font-weight: var(--font-display-weight);
line-height: var(--font-display-leading);
}
.prose p {
font-size: var(--font-body);
line-height: var(--font-body-leading);
}
.badge {
font-size: var(--font-caption);
letter-spacing: var(--letter-spacing-wide);
}A dense theme by re-binding roles
ExampleOverride the semantic layer in a scope; the value layer and components stay untouched.
[data-density="compact"] {
--font-display: var(--font-size-3xl); /* was 4xl */
--font-heading: var(--font-size-xl); /* was 2xl */
--font-body: var(--font-size-sm); /* was base */
}
/* Same <h1>, smaller render — no component change. */Per-brand override of the value layer
ExampleMulti-brand systems regenerate just the family token. Run the tool with a new Family name and swap one line.
/* Brand A (default) */
--font-family-base: "Inter", system-ui, ...;
/* Brand B — regenerate with Family name = "Söhne" */
[data-brand="b"] {
--font-family-base: "Söhne", system-ui, -apple-system, ...;
}
/* Sizes/weights stay shared; only the family forks. */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 the tool to emit semantic role tokens
Not supported — by designThere is no role-token option among the four controls. The generator stops at the raw scale (--font-size-*) plus the fixed weight/line-height/letter-spacing groups. Semantic roles like --font-display are aliases you author yourself — which is correct, because role naming is a system-specific decision.
Too few steps for your role count
RegenerateIf your roles need a 5xl display size but you generated only 7 steps (top step 3xl), the alias var(--font-size-5xl) resolves to nothing. Raise Steps (up to 12) and regenerate so the larger labels exist before you alias to them.
Mono family doesn't match your code font
Fixed value--font-family-mono is a fixed ui-monospace stack and isn't configurable here. For a custom code-font stack, generate one with system-font-stack-generator and override --font-family-mono in your semantic file.
You want tokens in JSON for Figma / a token store
Not supportedOutput is CSS :root only. There's no JSON or W3C Design Tokens export. For a cross-platform pipeline you copy the values out manually — the dedicated Style Dictionary guide for this tool walks the translation.
Regenerating drifts your token names
StableSame inputs produce the same output — the labels (xs…8xl) and value formula are deterministic. As long as you keep base, ratio, and steps constant, regenerating won't rename tokens, so your semantic aliases keep resolving.
Theming the value layer instead of the semantic layer
AvoidIt's tempting to override --font-size-base per theme, but that ripples through every alias unpredictably. Theme the semantic layer (--font-body: var(--font-size-sm)) so changes are explicit and scoped. The value layer should be a stable foundation.
Two roles aliasing the same step
Expected--font-caption and --font-code both aliasing var(--font-size-sm) is fine and common — they differ by family/weight, not size. The generator emits each size step once; multiple roles can point at the same one.
Letter-spacing roles needed beyond the 5 provided
Compose your ownThe tool ships 5 letter-spacing tokens (-0.05em to 0.05em). If a display role needs tighter than -0.05em, define your own in the semantic file (--font-display-tracking: -0.06em) rather than expecting the generator to add it.
Frequently asked questions
Does this generate semantic role tokens like --font-display?
No. It generates the raw scale (--font-size-*) plus fixed weight, line-height, and letter-spacing groups. You author the semantic layer (--font-display: var(--font-size-3xl)) yourself in a second block — there's no role-token option, and that boundary is intentional since roles are system-specific.
Why split into value and semantic layers?
Decoupling intent from value. Components reference roles (var(--font-heading)); a redesign re-binds the role once and every heading updates. If components referenced raw steps directly, every redesign would touch every component.
How many semantic roles should I define?
Match your component library — commonly display, heading, subheading, body, caption, code, label. 5–8 roles cover most design systems. The generator doesn't constrain this; you alias as many roles as you need against the generated steps.
How many size steps should the foundation have?
Enough to cover your largest display role plus a little headroom. Base 16, Major Third, 9 steps gives xs→5xl, which suits most product systems. Steps max out at 12 (xs→8xl).
Can I theme without recompiling?
Yes — that's the advantage of CSS custom properties. Re-bind the semantic layer in a [data-theme] or [data-density] scope and the same components render differently at runtime. No build step, unlike Sass variables.
Are the size tokens in rem or px?
rem, relative to your base. --font-size-base is 1.000rem and the rest scale from it. rem keeps the whole system responsive to user font-size preferences, which matters for accessibility audits.
Is the output fluid (clamp)?
No — static rem only. For a fluid foundation, generate the static scale here for reference, or use typography-scale-builder, which emits a fluid clamp() variant alongside the static one.
Can I use this for marketing and product surfaces both?
Yes. Generate one value layer, then author surface-specific semantic layers (e.g. a marketing scope that aliases --font-display to a larger step). Shared values, different role bindings per surface — the layered model handles exactly this.
What about the weight tokens — can I add a thin (100) weight?
The generator's weight set is fixed at 300–800. If you need 100/200/900, add them in your semantic/override file (--font-weight-thin: 100). The tool won't emit them, but custom properties compose freely.
How do I keep the foundation reproducible across rebuilds?
Record the four inputs (family, base, ratio, steps) in your docs. The output is deterministic — same inputs, same :root block — so anyone can regenerate the identical value layer.
Does it load fonts for the system?
No — it only names the family in --font-family-base. Load fonts separately with font-face-generator or google-fonts-css-generator, and consider a preload-tag-builder for the critical weights.
Is any of this sent to a server?
No. It's a generative, browser-only tool — no font upload, no account needed. The token block is built locally from your four inputs.
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.