How to convert a variable font to a static ttf
- Step 1Confirm you're on Pro (the freezer is gated) — The Variable Font Freezer is a **Pro-tier** tool. Free accounts can read a variable font's design space with [the OpenType features inspector](/font-tools/opentype-features-inspector) and [the metadata extractor](/font-tools/font-metadata-extractor), but actually writing the static file needs Pro. The free font-family file cap is 5 MB; Pro raises it to 50 MB, which covers every realistic variable Latin or CJK face.
- Step 2Drop your variable font — Any variable TTF, OTF, WOFF, or WOFF2 with an `fvar` table. The tool sniffs the format from the file's first bytes, decompresses WOFF (pako/zlib) or WOFF2 (wawoff2) to a flat sfnt, then checks for `fvar`. No `fvar` table means it isn't a variable font and you get a clear error instead of a broken output.
- Step 3Leave the axis box at the default — or understand what editing it does — The single option is a textarea, one axis per line, format `tag=value` (e.g. `wght=400`). It pre-fills `wght=400`. **Editing these values does not move the outlines** — opentype.js can't apply `gvar` deltas in-browser. The values you type are recorded in the result panel's `Axes baked` line for documentation; the glyph shapes stay at the default master either way.
- Step 4Run the freeze — The tool parses `fvar` for the axis list, then calls the table-removal routine that drops the eight variable tables and emits a brand-new sfnt with a rebuilt 12-byte header and 16-byte directory entries, tables re-sorted by tag and padded to 4-byte boundaries. Output MIME is `font/ttf`.
- Step 5Read the result panel before you ship — The panel shows `Axes baked` (each `tag=value`), `Frozen at default` (`yes`, or `no — visual outlines still at default` if you typed non-default values), `Removed tables`, and the size reduction. If `Frozen at default` says `no`, the file is static but the shapes are the default master — not the instance you asked for. Re-run with the defaults, or use fonttools for the real instance.
- Step 6Download the static TTF — The file is named `<stem>.static.ttf`. It's a flat, uncompressed sfnt — good for PDF libraries, desktop installers, and legacy engines. If you need a web-ready WOFF2 of this static face, run it through [the TTF to WOFF2 converter](/font-tools/ttf-to-woff2) afterward; the freezer never re-compresses on the way out.
What the freezer removes vs keeps
The freezer calls a single sfnt table-removal routine with a fixed list of eight tags. Everything else in the directory is copied through verbatim. Verified against lib/font/font-processor.ts (freezeVariableFont).
| Table | Removed? | What it did / why it's safe to drop or keep |
|---|---|---|
fvar | Removed | The axis + named-instance registry. Its presence is what marks a font 'variable'; removing it is what makes consumers treat the file as static. |
gvar | Removed | Per-glyph outline deltas — the bulk of variable-font overhead. Dropped without interpolation, so outlines fall back to the default master. |
HVAR | Removed | Horizontal metric variations. Advance widths revert to hmtx/default. |
MVAR | Removed | Metric variations (line height, x-height, cap height per axis position). Revert to head/OS/2 defaults. |
VVAR | Removed | Vertical metric variations (vertical writing modes). Revert to defaults. |
cvar | Removed | CVT variations for hinted variable fonts. Dropped along with the variation machinery. |
STAT | Removed | Style attributes table describing the design space for the OS font menu. Not needed once the face is single-instance. |
avar | Removed | Non-linear axis mapping (user value → internal coordinate). Dropped; only relevant while the font is still variable. |
glyf / loca | Kept | All glyph outlines, at default-master shape. Nothing is interpolated or deleted. |
GSUB / GPOS / kern | Kept | All OpenType layout — ligatures, kerning, contextual substitutions — survive untouched. |
cmap, name, head, OS/2, post, hhea, hmtx | Kept | Core sfnt tables copied through. Note: head.checkSumAdjustment is not recomputed (see edge cases). |
Input format → what comes out
The freezer accepts four input formats but always emits a flat TTF sfnt. The original sfnt 'flavor' (version tag) is preserved through the rewrite, even though the output is labelled .ttf.
| Input | Handled by | Output filename | Output MIME | Note |
|---|---|---|---|---|
| Variable TTF | Used as-is (already an sfnt) | <stem>.static.ttf | font/ttf | Cleanest path — flavor stays 0x00010000. |
| Variable OTF (CFF2) | Used as-is | <stem>.static.ttf | font/ttf | Flavor stays OTTO/CFF-flavored even though named .ttf. Rename to .otf if a strict consumer checks the extension. |
| Variable WOFF | Decompressed via pako (zlib) | <stem>.static.ttf | font/ttf | Per-table zlib inflated, sfnt rebuilt, then frozen. Output is uncompressed TTF, not WOFF. |
| Variable WOFF2 | Decompressed via wawoff2 | <stem>.static.ttf | font/ttf | WOFF2's brotli is undone first. Output is uncompressed TTF — re-run TTF→WOFF2 if you want a web file. |
Non-variable font (no fvar) | Rejected before output | — | — | Throws This font has no fvar table — it isn't a variable font. |
Cookbook
What the result panel and output file actually look like for common variable fonts. The size deltas below are illustrative of the mechanism (gvar dominates the savings); your numbers depend on axis count and glyph coverage.
Default freeze of a 1-axis Latin variable (the common case)
ExampleYou leave the axis box at its default. The freezer strips the eight variable tables and emits a static TTF at the default-master shape. This is the path that 'just works'.
Input: Inter.var.woff2 (variable, axis: wght 100–900, default 400) Option: wght=400 (left at default) Result panel: Axes baked: wght=400 Frozen at default: yes Removed tables: fvar, gvar, HVAR, MVAR, VVAR, cvar, STAT, avar Reduction: ~55% Download: Inter.static.ttf (static, no fvar)
You typed a non-default weight — read the warning
ExampleYou set wght=700 expecting a Bold static. The file IS static, but the outlines are still the default-master (400) shapes, because gvar deltas are not applied in-browser. The panel tells you.
Input: Inter.var.woff2 (default wght 400) Option: wght=700 Result panel: Axes baked: wght=700 Frozen at default: no — visual outlines still at default Removed tables: fvar, gvar, HVAR, MVAR, VVAR, cvar, STAT, avar What you got: a static TTF that LOOKS like Regular, not Bold. What you wanted: fonttools varLib.instancer (see below).
Bake a real instance with fonttools (desktop)
ExampleWhen you actually need the outlines at a specific axis position, the freezer is the wrong tool — varLib.instancer applies the gvar deltas. The freezer is for the default-master static case only.
# pip install fonttools brotli fonttools varLib.instancer Inter.var.ttf wght=700 \ -o Inter-Bold.ttf # Now Inter-Bold.ttf has Bold OUTLINES baked in, # fvar removed, and the rest of the variable tables # pinned to wght=700. Then convert to WOFF2 if needed.
WOFF2 in, static TTF out, then re-pack for the web
ExampleThe freezer never re-compresses. If your source is a WOFF2 and you want a static WOFF2, freeze first (decompresses + strips) then convert back.
Step 1 Freezer: RobotoFlex.var.woff2 -> RobotoFlex.static.ttf
(wawoff2 decompresses, 8 tables removed)
Step 2 TTF -> WOFF2 converter:
RobotoFlex.static.ttf -> RobotoFlex.static.woff2
(now web-ready, ~50–70% smaller than the TTF)Rename an OTF-flavored output
ExampleA CFF2 variable OTF freezes fine, but the output keeps its OTTO flavor while being named .static.ttf. Some installers and validators key off the extension — rename to match the flavor.
Input: SourceSerif.var.otf (CFF2-flavored, OTTO) Output: SourceSerif.static.ttf (still OTTO inside) # Rename so the extension matches the actual flavor: mv SourceSerif.static.ttf SourceSerif.static.otf
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.
You set a non-default axis value expecting baked outlines
By design — outlines stay at defaultTyping wght=700 does not produce Bold outlines. opentype.js can't apply gvar deltas client-side, so the static file always carries the default-master shapes. The result panel flags this explicitly with Frozen at default: no — visual outlines still at default. For a real instance, run fonttools varLib.instancer Inter.var.ttf wght=700.
Dropping a non-variable font on the freezer
Rejected — no fvarThe tool parses the table directory for fvar. If it isn't present, you get This font has no fvar table — it isn't a variable font. and no output. A plain static TTF, an icon font, or an already-frozen file all land here. That's intentional — freezing a static font is a no-op, so the tool refuses rather than handing you an identical file.
Output named .ttf but actually CFF/OTTO-flavored
Expected — rename if neededThe removal routine preserves the source sfnt's flavor (version tag). A CFF2 variable OTF (OTTO flavor) comes out as a valid CFF font that's still labelled .static.ttf with MIME font/ttf. Most engines read the flavor, not the extension, so it loads — but strict installers or validators that key off .ttf may complain. Rename to .otf.
head.checkSumAdjustment is not recomputed
By design — usually ignoredThe table-removal routine rebuilds the directory but does not recompute the head table's checkSumAdjustment field, which is a checksum over the whole font. Virtually every renderer ignores it (browsers, FreeType, DirectWrite). A strict validator like fontbakery or ots (OpenType Sanitiser, used in Chromium's font pipeline historically) may warn. Pass the output through fonttools ttx round-trip or ots-sanitize if you need a corrected checksum.
MVAR-driven metrics revert after freezing
Expected — metrics fall back to defaultsMVAR lets a variable font shift line height, x-height, or cap height as you move along an axis. Once MVAR is dropped, those metrics revert to the static values in head/OS/2. For most Latin UI fonts this is invisible. For fonts that lean on optical-size (opsz) metric compensation, frozen text may set slightly differently than the variable font did at a non-default position.
Output is bigger or barely smaller than expected
Expected — depends on gvar shareSavings come almost entirely from dropping gvar. A 5-axis Latin font is mostly gvar, so freezing cuts a lot. A CJK variable font is mostly glyf/loca (tens of thousands of glyphs) with a comparatively small gvar, so freezing may only shave a few percent. The reduction percentage in the panel is the truth for your specific file.
Frozen WOFF2 source comes out as a fat TTF
Expected — no re-compressionWOFF2 input is brotli-decompressed to an sfnt before freezing, and the output is a flat, uncompressed TTF. So a 90 KB variable WOFF2 can produce a 250 KB static TTF — the bytes are uncompressed, not bloated. Run the result through TTF→WOFF2 to get a web file that's typically smaller than the original variable WOFF2.
STAT removal and the OS font menu
By designSTAT describes the family's style axes for the operating-system font picker. Dropping it means the frozen face shows up as a standalone style rather than as a member of the variable family's axis grid. That's correct for a static fallback, but if you're producing a family of statics for desktop install, set the name table style fields per file (the freezer doesn't rewrite name).
File over the tier cap
Rejected — size limitFree accounts can't run the freezer at all (it's Pro-gated). On Pro the font file cap is 50 MB; Developer raises it to 1 GB. A huge CJK variable font near the Pro ceiling freezes but is slow because the whole sfnt is rebuilt in memory. If you're scripting many fonts, drive the local runner instead of the browser tab.
Frequently asked questions
Does the freezer respect the axis values I type?
Only for the record. The static file's outlines stay at the font's default-master shapes regardless of what you enter, because opentype.js can't apply gvar deltas in the browser. The values you type appear in the result panel's Axes baked line and drive the Frozen at default yes/no flag, but the glyphs don't move. For outlines baked at an exact position, use fonttools varLib.instancer on the desktop.
Why does the freezer help legacy browsers?
Variable-font support landed in Chrome 62, Firefox 62, Safari 11, and Edge 17 around 2018. Older Safari, Edge Legacy, IE11, and some embedded WebKit/Chromium views in native apps reject fvar and fail to load the font. A frozen static TTF has no fvar, so those engines accept it. Ship the static as a fallback in your @font-face src list — see the fallback pipeline guide.
Which tables exactly get removed?
Eight: fvar, gvar, HVAR, MVAR, VVAR, cvar, STAT, and avar. Nothing else is touched — glyf/loca, cmap, GSUB, GPOS, kern, name, head, OS/2, post, hhea, and hmtx are all copied through verbatim into a rebuilt sfnt directory.
Will freezing break my ligatures or kerning?
No. The freezer only removes variable-specific tables. GSUB (ligatures, contextual substitution) and GPOS/kern (kerning, mark positioning) are preserved exactly. If you want to verify them on the frozen output, run it through the OpenType features inspector or the kerning-pair auditor.
What input formats can I freeze?
TTF, OTF, WOFF, and WOFF2. WOFF is zlib-decompressed per-table and rebuilt into an sfnt; WOFF2 is brotli-decompressed via wawoff2 first. All four paths end at the same place: an sfnt that gets the eight tables stripped. The output is always an uncompressed TTF.
How much smaller is the static file?
It depends entirely on how much of the font was gvar. Latin variable fonts with several axes can shrink substantially because gvar dominates them; CJK variable fonts shrink far less because their bulk is glyph outlines, not deltas. The result panel reports the exact reduction for your file. The freezer's output is uncompressed TTF, so to compare like-for-like against the original WOFF2, convert the result with TTF→WOFF2 first.
Why is the output a .ttf when I uploaded an OTF?
The freezer always names the output <stem>.static.ttf and sets MIME font/ttf, but it preserves the original sfnt flavor. A CFF2 OTF stays CFF-flavored (OTTO) inside the .ttf-named file. Engines read the flavor and load it correctly; if a strict, extension-checking installer complains, rename it to .otf.
Do I need Pro to use the freezer?
Yes — writing the static file is a Pro-tier action. Free accounts can read the variable design space (axes, named instances) with the OpenType features inspector and pull metadata with the metadata extractor, but the freeze itself needs Pro. Pro also raises the font file cap from 5 MB to 50 MB.
Is the output a strictly valid OpenType font?
It's a valid sfnt that every shipping renderer loads, with one caveat: the head.checkSumAdjustment field isn't recomputed after the directory rewrite. Browsers and FreeType ignore it; a strict validator (fontbakery, ots-sanitize) may flag it. Round-trip through fonttools ttx or ots-sanitize if you need a corrected checksum.
Does the font ever get uploaded to JAD's servers?
No. The entire freeze — format detection, WOFF/WOFF2 decompression, fvar parsing, table removal, sfnt rebuild — runs in your browser via opentype.js, pako, and wawoff2. The binary stays on your device, which matters for unreleased or licensed type. Only a privacy-safe processed-count metric is recorded for signed-in stats.
Can I freeze a whole folder of fonts at once?
Not in the browser tab — the font tools process one file at a time (Pro batch is 20 for tools that support batching, but the freezer is single-file in the UI). For bulk freezing, pair the JAD runner and POST each file to http://127.0.0.1:9789/v1/tools/variable-font-freezer/run. The runner enforces the same tier limits and keeps every byte local.
Should I just freeze all my weights to static instead of shipping variable?
Usually no, if you use three or more weights from one family — variable typically wins on total bytes there. Freeze when you need a legacy fallback, a single weight, or a face for a pipeline that rejects fvar. The variable-vs-static tradeoffs guide walks the break-even, and the design-system policy guide covers when freezing belongs in your standard.
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.