How to wire variable font weights into design-system weight tokens
- Step 1Self-host the variable font behind your token system — Host the variable WOFF2 under your domain and note the URL. The builder writes it into the `src`; it never fetches the file. If you currently use Google Fonts, self-host first via [google-fonts-css-generator](/font-tools/google-fonts-css-generator).
- Step 2Enter the design-system family name — Use the CSS family your tokens reference (e.g. `Inter` or an alias like `AppSans`). It becomes the quoted `font-family` in all nine blocks and the filename stem. It needn't match the font's internal name.
- Step 3Point the Font URL at the hosted file — Paste the deployed path (default `/fonts/inter.var.woff2`). It's written verbatim into both `src` entries — no resolution, no validation.
- Step 4Generate the @font-face weight layer — The builder emits nine blocks Thin 100 → Black 900, each `font-weight === wght`, `font-style: normal`, `font-display: swap`. This is the binding layer your tokens sit on top of.
- Step 5Define your --font-weight-* tokens to match — Add `:root { --font-weight-regular: 400; --font-weight-medium: 500; --font-weight-bold: 700; ... }` using the nine round weights. Because the presets cover exactly those values, every token resolves to a real preset.
- Step 6Reference only tokens in components — Write `.card-title { font-weight: var(--font-weight-bold); }`. The component never sees `font-variation-settings` — the abstraction holds. Add the size/line-height tokens separately (see the related tool).
Suggested token → preset mapping
The nine emitted presets map cleanly onto a conventional weight-token scale. Token names are your choice; the right-hand values are what the builder actually emits.
| Suggested token | Resolves to font-weight | Generated preset (wght) |
|---|---|---|
--font-weight-thin | 100 | Thin (100) |
--font-weight-extralight | 200 | Extra Light (200) |
--font-weight-light | 300 | Light (300) |
--font-weight-regular | 400 | Regular (400) |
--font-weight-medium | 500 | Medium (500) |
--font-weight-semibold | 600 | Semi Bold (600) |
--font-weight-bold | 700 | Bold (700) |
--font-weight-extrabold | 800 | Extra Bold (800) |
--font-weight-black | 900 | Black (900) |
Layered abstraction: component → token → @font-face → axis
How a font-weight request flows from a component down to the variable axis. The builder owns only the third layer.
| Layer | Example | Owned by |
|---|---|---|
| Component | font-weight: var(--font-weight-bold) | Your component CSS |
| Token | --font-weight-bold: 700 | Your token file |
| @font-face | font-weight: 700 + "wght" 700 | This builder |
| Axis | interpolated glyph at wght 700 | The variable font |
What this builder owns vs the rest of the type system
Scope check — this tool only writes the weight @font-face layer.
| Type-system concern | This tool? | Where else |
|---|---|---|
| Weight @font-face blocks | Yes | — |
--font-weight-* tokens | No (you author them) | Author by hand / token build |
| Size / line-height tokens | No | css-variable-generator-font |
| Fluid clamp() sizes | No | typography-scale-builder |
| Dark-mode weight tweaks | No | dark-mode-font-adjuster |
Cookbook
The @font-face weight layer plus the token layer that sits on it. Together they give components a clean weight API.
Generated @font-face (the binding layer)
ExampleTwo of the nine blocks the builder emits — the foundation tokens resolve onto.
@font-face {
font-family: "AppSans";
src: url("/fonts/app-sans.var.woff2") format("woff2-variations"),
url("/fonts/app-sans.var.woff2") format("woff2");
font-weight: 500;
font-style: normal;
font-display: swap;
font-variation-settings: "wght" 500;
}
@font-face {
font-family: "AppSans";
src: url("/fonts/app-sans.var.woff2") format("woff2-variations"),
url("/fonts/app-sans.var.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
font-variation-settings: "wght" 700;
}The token layer you author on top
ExampleHand-written custom properties that resolve onto the generated presets.
:root {
--font-family-sans: "AppSans", system-ui, sans-serif;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold:600;
--font-weight-bold: 700;
}Components reference tokens only
ExampleNo font-variation-settings ever leaks into the component layer.
.card {
font-family: var(--font-family-sans);
font-weight: var(--font-weight-regular);
}
.card__title { font-weight: var(--font-weight-bold); }
.card__meta { font-weight: var(--font-weight-medium); }Completing the type system with sizes
ExampleThis tool covers weight only; add size/line-height tokens separately.
/* weights: generated here */
/* sizes: generate with css-variable-generator-font */
:root {
--font-size-base: 1rem;
--line-height-normal: 1.5;
}Dark-mode weight token override
ExampleHalation makes light-on-dark text look heavier; nudge the regular token down a step in dark mode (still a real preset).
@media (prefers-color-scheme: dark) {
:root { --font-weight-regular: 300; } /* was 400 */
}
/* 300 is a generated preset, so this resolves cleanly */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.
Your token scale uses non-round weights (e.g. 350, 450)
No matching presetThe builder emits only the nine round hundreds. A token like --font-weight-book: 450 won't resolve to a generated @font-face weight, so the browser falls back to weight matching against the nearest available preset. To support 450, add a font-variation-settings: "wght" 450 rule yourself; the cached file supports it even though the preset ladder doesn't.
Two tokens point at the same weight
200 OK — aliasingDefining both --font-weight-regular: 400 and --font-weight-body: 400 is fine — both resolve to the same generated Regular preset. The variable file is downloaded once regardless of how many tokens alias the same weight.
Token references a weight outside the font's axis
ClampedIf a token resolves to 100 or 900 but the font's wght axis is narrower, the browser clamps to the nearest axis end. The token still 'works' but renders at the clamped weight. Verify the real range with font-metadata-extractor before promising Thin or Black in your token docs.
You wanted italic weight tokens too
Not generatedEvery preset is font-style: normal. There's no italic ladder. For --font-weight-bold-italic-style tokens you must hand-author italic @font-face blocks (duplicate the set with font-style: italic plus the slant/ital axis value) — this tool won't emit them.
Expecting size and spacing tokens from this tool
Out of scopeThis builder only produces the weight @font-face layer. It does not emit --font-size-*, --line-height-*, or --letter-spacing-*. Generate those with css-variable-generator-font or typography-scale-builder and combine the outputs.
Family-name alias mismatch with tokens
Fallback font showsIf your --font-family-sans token says "AppSans" but you generated presets with family Inter, the @font-face won't match and components fall back to the system stack. Keep the Family name field identical to the family your token references.
Minifier strips the token comments but keeps values
200 OKCSS minifiers drop comments but preserve custom-property declarations and the font-variation-settings values. The generated header comment may vanish in production; the nine blocks and your tokens survive intact and still resolve correctly.
Theming swaps the whole font family at runtime
Regenerate per familyEach generated set is bound to one family name. If a theme switches to a different variable font, you need a second generated set for that family (different Family name + URL) and tokens that reference the active family. One generation per font family.
Frequently asked questions
How does this fit a design-token workflow?
It generates the @font-face binding layer: nine blocks mapping font-weight numbers to font-variation-settings: "wght" on one variable file. You then author --font-weight-* tokens that resolve to those numbers, and components reference the tokens. The result is a clean layering — component → token → @font-face → axis — with no axis syntax in component CSS.
Does the tool generate the custom properties too?
No. It emits only the @font-face weight presets. You author the --font-weight-* tokens yourself (a few lines), choosing names that map to the nine round weights. For size and line-height tokens, use css-variable-generator-font.
Why do the preset weights match a token scale so well?
The ladder is the conventional Thin/Extra Light/Light/Regular/Medium/Semi Bold/Bold/Extra Bold/Black scale at 100-step intervals — the same scale most design systems use for weight tokens. So each token maps 1:1 to a generated preset.
What if my token uses a non-round weight like 450?
There's no generated preset for it, so the browser weight-matches to the nearest one. To get exactly 450, add a font-variation-settings: "wght" 450 rule on the relevant selector — the cached variable file renders it even though the preset ladder doesn't include it.
Can I use an alias family name instead of the font's real name?
Yes — set the Family name field to whatever your --font-family-* token references (e.g. AppSans). The presets bind to that name; it doesn't have to match the font's internal name table. Just keep it identical to the token's family value.
Does theming with multiple fonts work?
Each generated set is tied to one family name and URL. For a theme that switches fonts, generate a second preset set for the other variable font and have your tokens reference the active family. One generation per family.
Are italic weight tokens supported?
No — all presets are font-style: normal. For italic tokens you must author italic @font-face blocks by hand (duplicate the set with font-style: italic and the slant/ital axis). This tool only emits the upright weight ladder.
Will all nine blocks bloat the token CSS?
No. The nine blocks repeat near-identical text and compress to a few hundred bytes under gzip/brotli. The real payload is the single variable file, shared across every token. That's far leaner than shipping nine static weight files.
How do I handle dark-mode weight adjustments?
Override the token value in a prefers-color-scheme: dark block (e.g. drop --font-weight-regular from 400 to 300). As long as the new value is one of the nine generated presets, it resolves cleanly. For the rationale and full adjustment set, see dark-mode-font-adjuster.
Does this tool produce a complete typography system?
No — only the weight layer. Combine it with size/line-height tokens from css-variable-generator-font and fluid sizing from typography-scale-builder for the full system.
Is there any upload or file limit?
No. It's a generative tool with two text inputs and no font upload, so the file-size tiers that gate binary-parsing tools don't apply. The output is plain CSS you can commit to your token files.
How do I keep the @font-face layer and the token layer in sync?
Commit both into the same typography file: the generated <family>-weight-presets.css blocks first, then your :root --font-weight-* declarations. Because the presets are deterministic and the tokens just reference the nine round weights, any change to the font (new family or URL) is a regenerate-and-replace of the @font-face layer with the token layer untouched — as long as the Family name field still matches your --font-family-* token.
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.