How to encode fluid font sizes as design tokens
- Step 1Define per-role bounds in your token source — List each role with its min/max px and a shared (or per-role) viewport range, e.g. `display: { min: 32, max: 64, minVw: 320, maxVw: 1280 }`, `body: { min: 16, max: 18, minVw: 320, maxVw: 1280 }`. This is your input matrix — one row per token.
- Step 2Generate the clamp() for each role — Run this tool once per role with that role's four numbers. For `display` (32→64 / 320→1280) it returns `clamp(2.000rem, 1.333rem + 3.333vw, 4.000rem)`. Copy the expression — it is the token value.
- Step 3Rename the variable to your role token — The output's `:root` rule uses `--font-size-fluid`. Rename it to the role: `--font-size-display: clamp(2.000rem, 1.333rem + 3.333vw, 4.000rem);`. The tool does not name tokens for you, so this rename is the manual glue.
- Step 4Or replicate the math in your build script — For a CI pipeline, port `buildClampExpression(minPx, maxPx, minVw, maxVw, 16)` — it is a few lines: slope = (max-min)/(maxVw-minVw), vw term = slope×100, intercept rem = (min - slope×minVw)/16, bounds = min/16 and max/16, everything `.toFixed(3)`. Iterate your role matrix and emit a token per row.
- Step 5Feed tokens into Style Dictionary or a CSS file — Drop the resolved clamp() strings into your token JSON as `value`, or write them straight into a `:root` block. Style Dictionary treats them as opaque CSS values and passes them through to the web platform unchanged.
- Step 6Theme by overriding bounds, not the expression — For a 'compact' or 'comfortable' theme, regenerate each role with different min/max and emit a themed `:root` (e.g. under `[data-theme="compact"]`). Each theme is its own pass of the four inputs. Reference the tokens via `var(--font-size-display)` so components inherit whichever theme is active.
A per-role token matrix and the exact clamp() each yields
Run the generator once per row. Viewport range shared at 320 → 1280 for these examples; root size fixed at 16, output to 3 decimals.
| Role token | Min/Max px | Generated clamp() |
|---|---|---|
--font-size-display | 32 → 64 | clamp(2.000rem, 1.333rem + 3.333vw, 4.000rem) |
--font-size-heading | 24 → 40 | clamp(1.500rem, 1.167rem + 1.667vw, 2.500rem) |
--font-size-subheading | 20 → 28 | clamp(1.250rem, 1.083rem + 0.833vw, 1.750rem) |
--font-size-body | 16 → 18 | clamp(1.000rem, 0.958rem + 0.208vw, 1.125rem) |
--font-size-small | 14 → 15 | clamp(0.875rem, 0.854rem + 0.104vw, 0.938rem) |
What the tool emits vs what a token pipeline needs
The gap between the generator's single-variable output and a named per-role token set — and how to close it.
| Token-pipeline need | Generator behaviour | How to bridge |
|---|---|---|
| One value per role | One clamp() per run | Run once per role |
| Named variable per role | Always --font-size-fluid | Rename after copy, or script it |
| Whole modular scale at once | Not supported (single size) | typography-scale-builder |
| Themeable bounds | No theme concept | Re-run per theme, emit scoped :root |
Style Dictionary value | Plain CSS string | Paste the clamp() as the token value |
buildClampExpression — the formula to port
Exactly what the tool computes internally (lib/font/font-utils.ts), so your build script matches byte-for-byte.
| Quantity | Formula | For display 32→64 / 320→1280 |
|---|---|---|
| Min rem (floor) | minPx / 16 | 2.000rem |
| Max rem (ceiling) | maxPx / 16 | 4.000rem |
| Slope | (maxPx - minPx) / (maxVw - minVw) | 32 / 960 = 0.03333… |
| vw term | slope × 100 | 3.333vw |
| Intercept rem | (minPx - slope × minVw) / 16 | 1.333rem |
Cookbook
Recipes for wiring the generator's output into a token system. The clamp() strings are the exact ones the tool returns for the stated inputs.
Per-role tokens in a :root block
ExampleThe most direct approach: run the tool per role, rename --font-size-fluid to the role name, and collect them in one :root. Components reference var(--font-size-*).
:root {
--font-size-display: clamp(2.000rem, 1.333rem + 3.333vw, 4.000rem);
--font-size-heading: clamp(1.500rem, 1.167rem + 1.667vw, 2.500rem);
--font-size-body: clamp(1.000rem, 0.958rem + 0.208vw, 1.125rem);
}
h1 { font-size: var(--font-size-display); }
h2 { font-size: var(--font-size-heading); }
p { font-size: var(--font-size-body); }Style Dictionary token JSON
ExampleDrop the generated clamp() string in as the token value. Style Dictionary passes it through to the CSS platform unchanged — it is just an opaque value.
{
"font": {
"size": {
"display": { "value": "clamp(2.000rem, 1.333rem + 3.333vw, 4.000rem)" },
"heading": { "value": "clamp(1.500rem, 1.167rem + 1.667vw, 2.500rem)" },
"body": { "value": "clamp(1.000rem, 0.958rem + 0.208vw, 1.125rem)" }
}
}
}Themed bounds: compact vs comfortable
ExampleEach theme is its own pass of the four inputs. Scope the resulting :root to a data attribute so the active theme picks the right clamp().
/* default theme */
:root { --font-size-body: clamp(1.000rem, 0.958rem + 0.208vw, 1.125rem); }
/* compact: smaller max (16 → 17 instead of 16 → 18) */
[data-theme="compact"] {
--font-size-body: clamp(1.000rem, 0.979rem + 0.104vw, 1.063rem);
}
/* comfortable: larger max (16 → 20) */
[data-theme="comfortable"] {
--font-size-body: clamp(1.000rem, 0.917rem + 0.417vw, 1.250rem);
}Port the math into a build script
ExampleFor CI, replicate buildClampExpression and iterate your role matrix — no copy-paste, deterministic output, clean diffs.
function clamp(minPx, maxPx, minVw, maxVw, root = 16) {
const slope = (maxPx - minPx) / (maxVw - minVw);
const vw = slope * 100;
const intercept = (minPx - slope * minVw) / root;
const s = vw >= 0 ? '+' : '-';
return `clamp(${(minPx/root).toFixed(3)}rem, ${intercept.toFixed(3)}rem ${s} ${Math.abs(vw).toFixed(3)}vw, ${(maxPx/root).toFixed(3)}rem)`;
}
const roles = {
display: [32, 64, 320, 1280],
heading: [24, 40, 320, 1280],
body: [16, 18, 320, 1280],
};
for (const [name, args] of Object.entries(roles))
console.log(`--font-size-${name}: ${clamp(...args)};`);Single ad-hoc token, copied straight from the tool
ExampleIf you only need one fluid token, the generator's :root output is already usable — just rename the variable.
/* Tool output, verbatim */
:root { --font-size-fluid: clamp(2.000rem, 1.333rem + 3.333vw, 4.000rem); }
/* Renamed to your role token */
:root { --font-size-display: clamp(2.000rem, 1.333rem + 3.333vw, 4.000rem); }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.
The tool emits only one variable name
By designEvery run writes --font-size-fluid, not a per-role name. For a token set you must rename it per role after copying, or replicate the math in a script that names tokens itself. The generator has no field for the variable name — it is single-purpose.
Root font size assumed to be 16
By designTokens generated here divide Min px by 16 for the rem floor. If your design system's root is not 16, the rem token values will not visually equal the px you intended. Standardise the root at 16 (the common default) so the tokens are accurate, or convert with your real root in a custom script.
Per-token viewport ranges drift between roles
Avoidable — keep one rangeNothing stops you giving each role a different Min vw / Max vw, but mixed ranges make a token set harder to reason about (display caps at 1440, body at 768). Most systems pick one global viewport range and vary only min/max px per role. Consistency wins; vary the size, not the range.
Theme override forgets to re-pin the ceiling
Process riskWhen you regenerate a token for a 'comfortable' theme with a bigger Max px, the clamp() ceiling rises automatically — good. But if you only change the intercept by hand instead of regenerating, the ceiling stays at the old value and the size caps early. Always regenerate the full clamp() per theme rather than editing one term.
Style Dictionary treats the value as opaque
ExpectedStyle Dictionary does not parse or validate the clamp() — it passes the string through to the web platform. That means a malformed expression (e.g. one you hand-edited) ships unchecked. Generate the value here so it is well-formed, and let the tool's .toFixed(3) rounding keep it clean.
Max vw not greater than Min vw for a role
Error — rejectedGenerating any single role token throws Max viewport must be greater than min viewport. if Max vw ≤ Min vw. In a token matrix this usually means a typo in one row — fix that row's viewport range before regenerating.
Negative slope token from inverted min/max
By design — verify before shippingIf a role's Max px is below its Min px (a data-entry slip in the matrix), the token clamp() gets a negative vw slope and the text shrinks as the screen grows. The tool does not reject this — review each generated token to confirm Max px > Min px for every role.
Non-web platforms can't use clamp() directly
Platform limitationclamp() is a CSS function; iOS/Android UI layers have no equivalent. A token whose value is a clamp() string is web-only. For cross-platform tokens, store the min/max px and viewport range as raw numbers and let each platform's build compute its own responsive sizing — the web build can still call this tool's math.
Whole-scale needs, not per-role
Use the sibling toolIf you actually want a modular scale (every step from xs to 4xl) as fluid tokens, this single-size generator is the wrong tool — you would run it many times. The typography-scale-builder emits a fluid clamp() per step in one pass, ready to tokenise.
Frequently asked questions
Can the tool output named per-role tokens directly?
No. It emits one :root { --font-size-fluid: ... } per run. For a per-role token set, run it once per role and rename the variable, or replicate its buildClampExpression math in your build script and name the tokens there.
How do I make the output deterministic for version control?
It already is. The root size is fixed at 16 and every number is rounded with .toFixed(3), so identical inputs always produce the identical string. That gives clean, stable diffs when you commit generated tokens.
Does putting clamp() in a token cost anything at runtime?
No. Browsers resolve clamp() in microseconds. The only cost is the few characters of the string in your CSS; there is no per-frame computation beyond normal layout.
Should every role share one viewport range?
Usually yes. Most systems use a single global Min vw / Max vw and vary only the per-role min/max px. Per-role ranges are possible but make the token set harder to reason about. Keep the range constant; change the size.
How do I theme the fluid tokens (compact / comfortable)?
Regenerate each role with different min/max px per theme and emit a scoped :root (e.g. [data-theme="compact"]). Reference tokens via var(--font-size-*) so components inherit whichever theme is active. Always regenerate the whole clamp(), never hand-edit one term.
Can I use these tokens in Style Dictionary?
Yes. Put the generated clamp() string as the token's value. Style Dictionary treats it as an opaque CSS value and emits it unchanged on the web platform. Because it does not validate the string, generate it here so it is well-formed.
What if I need a whole scale, not just named roles?
Use the typography-scale-builder. It takes a base size and modular ratio and emits a fluid clamp() per step (xs → 4xl) in a single run, plus a static rem scale — ideal for tokenising an entire scale at once.
Why does the tool assume a 16px root?
It hardcodes the root size to 16 when converting px to rem. 16px is the near-universal browser default, so the tokens are accurate for standard setups. If your html root differs, the rem values will not match your intended px — standardise to 16 or compute with your own root in a script.
Can I generate the tokens in CI instead of clicking?
Yes. Port buildClampExpression(minPx, maxPx, minVw, maxVw, 16) — it is a handful of lines (slope, vw term, intercept, two bounds, all .toFixed(3)). Iterate your role matrix and emit one token per row. The cookbook above has a ready-to-paste version.
Do these clamp() tokens work on iOS / Android?
No — clamp() is CSS-only. Store the raw min/max px and viewport numbers as the cross-platform token and let each platform's build compute its own responsive size. The web build can reuse this tool's math; native builds cannot consume the clamp() string.
How do I avoid shipping a broken token?
Generate every value with the tool (or its exact math) rather than hand-editing, and confirm each role has Max px > Min px so no token gets a negative slope. The tool's only hard guard is Max vw > Min vw; the min/max-px sanity check is on you.
Is any data uploaded when generating tokens?
No. The tool is fully client-side with no file input — it just runs the clamp() arithmetic in the browser. Nothing about your design system leaves your machine.
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.