How to trim variable font axis ranges and get the fonttools command
- Step 1Upload your variable font — Drop a TTF, OTF, WOFF, or WOFF2 variable font onto the tool. WOFF/WOFF2 are decompressed to a raw sfnt buffer first, then the `fvar` table is located by tag. The file is read entirely in your browser.
- Step 2Confirm the font actually has axes — If there's no `fvar` table the tool stops with **This font has no fvar table — it isn't a variable font.** Run a static font through the [variable-font-freezer](/font-tools/variable-font-freezer) instead — there's nothing to optimise on a static file.
- Step 3Read the per-axis recommendation — Each axis row shows `tag`, human-readable `name`, `current_min`, `current_max`, `recommended_min`, `recommended_max`, and `range_savings_pct`. The recommendation comes from the smallest and largest value any **named instance** uses on that axis — not from any CSS you paste (the tool has no input for CSS).
- Step 4Check the Best axis savings metric — The result panel shows **Axes analysed** and **Best axis savings** (the maximum `range_savings_pct`). If Best axis savings is 0%, the named instances already span the full declared range and trimming buys you nothing.
- Step 5Copy the fontTools command — The `fonttools_command` field is a full `fonttools varLib.instancer tag=min:max … yourfont.ttf` string. It uses your uploaded file's name as the target argument and one `tag=min:max` pair per axis.
- Step 6Run the command on the desktop and re-measure — Install fontTools (`pip install fonttools`), run the emitted command against your source font, then re-compress to WOFF2 with [ttf-to-woff2](/font-tools/ttf-to-woff2). Compare before/after sizes with [glyph-count-analyzer](/font-tools/glyph-count-analyzer) to confirm the real saving.
What the report contains per axis
Every axis in the fvar table produces one row with these fields. Recommended values are derived from named instances only.
| Field | Source | Meaning |
|---|---|---|
tag | fvar axis record | The 4-character axis tag (wght, wdth, slnt, opsz, or a custom CAPS tag like GRAD) |
name | name table (Windows English record) | Human-readable axis label; falls back to the tag if no name record exists |
current_min / current_max | fvar axis record (fixed 16.16, ÷65536) | The font's declared axis range as authored by the type designer |
recommended_min | min across named-instance coordinates | Smallest value any named instance uses on this axis; defaults to the axis default if no instance touches it |
recommended_max | max across named-instance coordinates | Largest value any named instance uses on this axis; defaults to the axis default if no instance touches it |
range_savings_pct | computed | round((1 − (recMax−recMin) / (curMax−curMin)) × 100), floored at 0; 0 when the declared range is also 0 |
Top-level output fields
Besides the per-axis array, the JSON carries two more keys plus result-panel metrics.
| Key / metric | Type | What it is |
|---|---|---|
axes | array | One object per axis, with the fields in the table above |
fonttools_command | string | fonttools varLib.instancer tag=recMin:recMax … <your filename> — paste into a terminal |
note | string | States that the trimming must run via fontTools because the JS ecosystem has no complete VarLib equivalent |
| Axes analysed (metric) | number | Count of axis rows in the report |
| Best axis savings (metric) | percentage | The maximum range_savings_pct across all axes; 0% if nothing can be trimmed |
Cookbook
Concrete fvar shapes and the exact report they produce. The recommendation always tracks the named instances, never the declared range.
Inter variable with a full instance ladder
ExampleInter.var declares wght 100–900 and ships named instances from Thin (100) through Black (900). Because the instances span the full declared range, the optimiser recommends the same range and reports 0% savings — there is nothing to trim, and the tool says so honestly.
fvar axis: wght min 100 default 400 max 900
named instances: Thin 100 … Black 900
report.axes[0]:
{
"tag": "wght",
"name": "Weight",
"current_min": 100, "current_max": 900,
"recommended_min": 100, "recommended_max": 900,
"range_savings_pct": 0
}
Best axis savings: 0%Wide weight axis, narrow instance ladder
ExampleA foundry font declares wght 1–1000 but only names instances at Regular (400) and Bold (700). The optimiser tightens the range to 400–700 and computes the saving from the declared 999-unit span.
fvar axis: wght min 1 default 400 max 1000 named instances: Regular 400, Bold 700 range = 1000 − 1 = 999 trimmed = 700 − 400 = 300 savings = round((1 − 300/999) × 100) = 70% report.axes[0].recommended_min = 400 report.axes[0].recommended_max = 700 report.axes[0].range_savings_pct = 70
The emitted fontTools command
ExampleFor a two-axis font (weight + width), the tool joins one tag=min:max pair per axis and appends your uploaded file's name. Paste it verbatim after installing fontTools.
report.fonttools_command: fonttools varLib.instancer wght=400:700 wdth=100:100 MyFont.ttf # on the desktop: pip install fonttools fonttools varLib.instancer wght=400:700 wdth=100:100 MyFont.ttf # → MyFont-instance.ttf with the trimmed axis space
An axis no instance touches
ExampleA font has an opsz (optical size) axis 8–144 but none of its named instances pin opsz — they all leave it at the default. With no used values, both recommendations fall back to the axis default, pinning the axis to a single point.
fvar axis: opsz min 8 default 14 max 144 named instances: none set opsz usedValues = [] → recommended_min = recommended_max = default(14) range_savings_pct = round((1 − 0/136) × 100) = 100 fonttools fragment: opsz=14:14 (axis pinned to 14)
Verifying the real saving after the run
ExampleThe percentage is a range-coverage figure, not a guaranteed file-size delta. Re-compress and measure to see the actual byte saving — gvar deltas don't shrink perfectly linearly with axis range.
before: Source.ttf (variable) → ttf-to-woff2 → 312 KB
after: fonttools varLib.instancer wght=400:700 Source.ttf
→ Source-instance.ttf → ttf-to-woff2 → 214 KB
actual WOFF2 saving ≈ 31% (report said 70% range coverage on wght)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.
Static font with no fvar table
Error: not a variable fontIf the file has no fvar table the tool throws This font has no fvar table — it isn't a variable font. Static fonts have nothing to optimise; there are no axes to trim.
No file uploaded
Error: upload requiredRunning with no file throws Upload a variable font. The optimiser is upload-driven — there is no generative or CSS-paste mode.
Variable font with zero named instances
Recommends the axis defaultsWhen fvar declares axes but lists no named instances, every axis has an empty set of used values, so both recommended_min and recommended_max fall back to the axis default. The fontTools command pins each axis to its default point, which is effectively a freeze. If that's your goal, the variable-font-freezer is the direct tool.
Declared axis range is a single point
0% savings on that axisIf an axis has min == max (a degenerate axis), currentRange is 0 and range_savings_pct is forced to 0 to avoid dividing by zero — even though the recommended range is also a point.
File over the tier size limit
400: file too largeFree tier rejects files over 5 MB; Pro allows 50 MB; Developer 1 GB. This tool requires the Pro tier to run, so the practical ceiling is 50 MB unless you're on Developer.
WOFF2 input
Decompressed first, then readWOFF/WOFF2 inputs are decompressed to a raw sfnt buffer before the fvar table is located, so a compressed variable font works exactly like a raw TTF — the axis ranges are identical.
Custom (non-registered) axes
Reported like any axisParametric and custom axes such as GRAD, XOPQ, YOPQ, or MONO are read straight from the fvar record and appear in the report with their own rows. The recommendation logic is identical: it tracks whatever the named instances use.
Instances that exceed the declared min/max
Recommendation can equal current rangeIf a named instance sits at the very edge of the declared range (e.g. an instance at wght 100 when min is 100), recommended_min equals current_min and that axis shows 0% trim — the instance ladder already needs the full span.
Expecting a downloadable trimmed font
JSON report onlyThe output is application/json plus a <font>.axis-ranges.json filename — never a .ttf. The actual trimming is performed by the emitted fontTools command on your machine; the tool's note field says exactly this.
Frequently asked questions
Does this tool actually shrink my font?
No. It reads the fvar table and produces a JSON report plus a fonttools varLib.instancer command. The real trimming happens when you run that command with Python's fontTools on the desktop. Browser-side variable subsetting isn't feasible — there's no complete VarLib equivalent in JavaScript.
Where do the recommended ranges come from?
From the named instances inside the font's fvar table. For each axis, recommended_min is the smallest value any named instance uses and recommended_max is the largest. The tool has no field for pasting your CSS — the recommendation is purely instance-driven.
What if my CSS uses weights between two named instances?
The recommended range spans the full instance ladder, so any value between the lowest and highest named instance is covered. If you use a weight outside that span, widen the range in the emitted command before running fontTools.
Why does my report say 0% savings?
Because the named instances already span the entire declared axis range. Fonts like Inter ship Thin (100) through Black (900) matching the declared 100–900 wght axis, so there's no slack to trim.
How is range_savings_pct calculated?
round((1 − (recMax − recMin) / (curMax − curMin)) × 100), floored at 0. If the declared range is 0 the result is 0 to avoid a divide-by-zero. It's a measure of axis-range coverage, not a guaranteed file-size reduction.
Does range_savings_pct equal the file-size saving I'll get?
Not exactly. gvar variation deltas don't shrink perfectly linearly with axis range. A 70% range trim might yield a 30–50% WOFF2 reduction depending on glyph complexity. Re-measure after running the command.
What tier do I need?
The Axis Range Optimiser requires the Pro tier. Free-tier accounts can use it on files up to 5 MB only after upgrading; Pro raises the limit to 50 MB and Developer to 1 GB.
Will this work on WOFF2 directly?
Yes. WOFF and WOFF2 inputs are decompressed to a raw sfnt buffer before the fvar table is parsed, so a compressed variable font reports the same axes as the raw TTF.
Can I just freeze the font to one instance instead?
Yes — if you only need a single weight, the variable-font-freezer bakes one instance to a static TTF in the browser. Use the optimiser when you want to keep a range but trim its edges.
Does the command include every axis?
Yes. The fonttools_command joins one tag=recMin:recMax pair per axis in the fvar table, then appends your uploaded file's name as the target argument.
Is my font uploaded to a server?
No. The fvar parse runs entirely in your browser via a hand-rolled DataView parser. Nothing about the font is transmitted.
What's the output filename?
<your-font-stem>.axis-ranges.json. The content type is application/json — it's a report you read and act on, not a font you install.
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.