How to cut unused axes down to size on big variable fonts
- Step 1Upload the superfamily — Drop Roboto Flex (or any many-axis variable font) onto the tool. The file is read in-browser; WOFF2 is decompressed to a raw sfnt before the `fvar` table is located by tag.
- Step 2Scan the axis table top to bottom — Every axis gets a row: `tag`, `name`, `current_min`, `current_max`, `recommended_min`, `recommended_max`, `range_savings_pct`. On a 13-axis font you'll see 13 rows. Parametric axes like `GRAD` and `XOPQ` appear with their CAPS tags.
- Step 3Find the high-savings axes — Axes no named instance moves show `recommended_min == recommended_max == default` and a high `range_savings_pct` (often 100% on a wide parametric axis). Those are your biggest wins — they get pinned to a single point.
- Step 4Note the keep-range axes — Axes whose named instances span a real range (usually `wght`, sometimes `opsz`) keep a non-degenerate recommended range. If their instances cover the full declared span the row shows 0% — leave it alone.
- Step 5Copy the all-axes command — The `fonttools_command` covers every axis in one line: a `tag=min:max` pair for each, ending with your file name. Pinned axes appear as `tag=default:default`.
- Step 6Instance on the desktop, then re-host — Run the command with fontTools, re-compress with [ttf-to-woff2](/font-tools/ttf-to-woff2), and wire up the result with [font-face-generator](/font-tools/font-face-generator). Sanity-check coverage with [character-coverage-map](/font-tools/character-coverage-map) — pinning axes never removes glyphs, only variation data.
How recommendations behave on a many-axis font
Three axis archetypes you'll see on a superfamily, and what the optimiser does with each.
| Axis situation | recommended_min / recommended_max | Typical range_savings_pct |
|---|---|---|
| Instances span the full declared range (e.g. wght 100–900 on a full ladder) | Equal to current_min / current_max | 0% (nothing to trim) |
| Instances span part of the range (e.g. wght 400–700 only) | Tightened to the used min/max | Proportional to the unused span |
| No instance moves the axis (most parametric axes) | Both = axis default (pinned) | Up to 100% on a wide axis |
| Degenerate axis (min == max in fvar) | Both = that single value | 0% (range is zero, guarded) |
Roboto-Flex-style axis inventory (illustrative)
Representative axes on a parametric superfamily. Actual declared ranges come from the font you upload; the optimiser reads them from fvar.
| Tag | Name | Registered? | Commonly set by sites? |
|---|---|---|---|
wght | Weight | Yes | Yes — usually a range |
wdth | Width | Yes | Sometimes |
opsz | Optical size | Yes | Sometimes (often via auto) |
slnt | Slant | Yes | Rarely |
GRAD | Grade | No (custom) | Rarely — good pin candidate |
XOPQ / YOPQ | Parametric opacity (x/y) | No (custom) | Almost never — pin |
XTRA / YTAS / YTDE | Parametric tracking / ascender / descender | No (custom) | Almost never — pin |
Cookbook
How a thirteen-axis font collapses to a lean instance once the unused parametric axes are pinned. Ranges shown are illustrative of fvar shapes the parser reads.
Pinning every parametric axis at once
ExampleA site uses only weight and optical size. Every parametric axis is left at default by the named instances, so the optimiser pins each to its default and keeps only wght and opsz as ranges. The single command does all thirteen.
fonttools_command (abridged): fonttools varLib.instancer \ wght=300:700 wdth=100:100 opsz=14:36 slnt=0:0 \ GRAD=0:0 XOPQ=96:96 YOPQ=79:79 XTRA=468:468 \ YTAS=750:750 YTDE=-203:-203 YTLC=514:514 YTUC=712:712 \ RobotoFlex.ttf
A parametric axis showing 100% savings
ExampleGRAD (grade) is declared -200 to 150 but no named instance moves it. The used-value set is empty, so both recommendations fall to the default and the axis pins, reporting full range coverage saved.
fvar axis: GRAD min -200 default 0 max 150 named instances: none set GRAD usedValues = [] → recommended_min = recommended_max = 0 range = 150 − (−200) = 350 range_savings_pct = round((1 − 0/350) × 100) = 100 fragment: GRAD=0:0
Keeping a real weight range
ExampleThe named instances run Light (300) to Bold (700) on a wght axis declared 100–1000. The optimiser keeps 300–700 and reports the saving against the 900-unit declared span.
fvar axis: wght min 100 default 400 max 1000 named instances: Light 300 … Bold 700 range = 1000 − 100 = 900 trimmed = 700 − 300 = 400 range_savings_pct = round((1 − 400/900) × 100) = 56 fragment: wght=300:700
Best axis savings metric on a superfamily
ExampleThe result panel's Best axis savings is the single largest per-axis percentage. On a superfamily where a wide parametric axis pins to a point, that figure is often 100% even though wght stays a range.
Axes analysed: 13 range_savings_pct values: [56, 0, 41, 100, 100, 100, 100, …] Best axis savings: 100% ← from a pinned parametric axis
Why opsz sometimes can't be trimmed
ExampleIf the font's named instances explicitly set opsz across its full 8–144 declared range (some optical families do), the recommendation matches the declared range and the axis reports 0% — pinning it would change rendering.
fvar axis: opsz min 8 default 14 max 144 named instances set opsz: 8, 14, 36, 144 recommended_min = 8, recommended_max = 144 range_savings_pct = 0 (instances need the whole axis)
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.
Font has fvar but no named instances
All axes pin to defaultWith no named instances, every axis has an empty used-value set, so the recommendation collapses to the axis default and the command pins everything. That's a full freeze in disguise — consider variable-font-freezer if a single static instance is all you want.
Static (non-variable) font
Error: not a variable fontNo fvar table throws This font has no fvar table — it isn't a variable font. A superfamily that's already been instanced to static has no axes left to analyse.
No file uploaded
Error: upload requiredThrows Upload a variable font. The optimiser always needs a file — there is no generative mode.
Superfamily over 50 MB on Pro
400: file too largePro caps uploads at 50 MB. Most variable superfamilies are well under that even uncompressed, but a giant CJK variable font may need the Developer tier (1 GB limit).
Axis named only by its tag
Tag shown as the nameIf a custom axis has no English name record, the name field falls back to the raw 4-character tag. The math is unaffected — it's purely a display fallback.
Pinning an axis changes default appearance
Verify rendering after instancingPinning an axis to its default is correct only if you never set it in CSS. If you actually animate GRAD for dark mode, don't pin it — widen that axis in the emitted command before running fontTools.
Expecting axes removed, not pinned
Pinned to a point, not deletedvarLib.instancer with tag=value:value pins an axis to a single coordinate, removing its variation deltas. The axis record may remain as a degenerate point depending on fontTools options; the optimiser only supplies ranges, not the full instancer flag set.
Re-running on an already-trimmed font
Reports 0% across the boardAfter fontTools removes the slack, re-uploading the trimmed font shows recommended ranges equal to the (now tighter) current ranges, so every axis reports 0% — confirmation the trim landed.
Frequently asked questions
Roboto Flex has thirteen axes — will the tool list them all?
Yes. The fvar parser reads every axis record, registered or parametric, and emits one report row per axis plus one tag=min:max pair in the command. Nothing is hidden.
Does pinning an axis remove glyphs?
No. Pinning removes variation data (gvar deltas) for that axis, not glyphs. Run character-coverage-map on the result to confirm every codepoint still resolves.
Why are parametric axes the biggest wins?
Because almost no site sets GRAD, XOPQ, YOPQ, or XTRA in CSS, and the named instances usually leave them at default. An axis no instance moves collapses to its default and reports a high savings percentage.
Can I keep optical size as a range?
Yes — if the font's named instances span an opsz range, the optimiser keeps it. If they don't, opsz pins to its default; widen it manually in the command if you rely on font-optical-sizing: auto.
Does the tool produce a smaller font directly?
No. It outputs JSON plus a fonttools varLib.instancer command. You run that command with Python's fontTools to produce the trimmed font, then re-compress with ttf-to-woff2.
What's the single number that tells me if it's worth it?
The Best axis savings metric — the largest per-axis range_savings_pct. On a superfamily with unused parametric axes this is frequently 100%, signalling a big trim is available.
Why might an axis show 0% on a superfamily?
Its named instances span the full declared range, so there's no slack. Common for wght on fonts with a complete Thin-to-Black ladder.
Is the recommendation based on my CSS usage?
No. There's no CSS input. The recommendation is derived entirely from the font's named instances. If your usage differs, edit the emitted command before running it.
What tier is required?
Pro. The file-size ceiling is 5 MB on Free, 50 MB on Pro, and 1 GB on Developer.
Will WOFF2 superfamilies work?
Yes — they're decompressed to a raw sfnt buffer before the fvar table is read, so the reported axes match the uncompressed font.
What if I want to compare variable vs static first?
Use the size comparator sibling tool to decide whether a variable font is even the right call before optimising its axes. If you only need one or two weights, a static export may be smaller overall.
Is my font sent anywhere?
No. The fvar parse is local. The report and command are generated in your browser from bytes that never leave it.
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.