Fonts

Convert TTF to WOFF2 for the Web

By the Smol team9 min read

To convert TTF to WOFF2, run fonttools ttLib.woff2 compress -o Lato.woff2 Lato-Regular.ttf. On a 656,568-byte TTF that produced 212,004 bytes, a 67.7% cut with every glyph and every outline preserved exactly.

Then do the thing that actually matters. Subsetting the same font to the Latin range it needs took it to 28,404 bytes, 86.6% smaller again. The format change is worth 444 KB. Subsetting is worth another 184 KB on top, and almost every “convert TTF to WOFF2” page stops before that point. Measured on 2026-09-26 with fontTools 4.61.1.

What does WOFF2 actually get you over TTF and WOFF?

A different compressor and a smarter layout of the same data. WOFF 1.0 wraps font tables in Flate. WOFF2 uses Brotli plus a content-aware preprocessing step that transforms the glyf and loca tables before compression, which is why it wins by a margin that plain zlib cannot reach. That description comes straight from the WOFF File Format 2.0 specification, a W3C Recommendation dated 08 August 2024.

FormatBytesvs TTFCompression
TTF (static, 3,023 glyphs)656,568—None
WOFF 1.0308,724−53.0%Flate
WOFF2212,004−67.7%Brotli

WOFF2 came out 31.3% smaller than WOFF 1.0 on this font, which lines up with the figure usually quoted for the format. It is also genuinely lossless. Decompressing our WOFF2 back to a TTF and comparing produced identical glyph outlines by hash, identical hmtx metrics and an identical character map. Nothing about the typeface changes. Only the transport does.

Browser support stopped being a question years ago. From MDN’s browser compatibility data for @font-face: WOFF2 landed in Chrome 36, Edge 14, Firefox 39, Safari 10 and iOS Safari 10. The only notable browser that never got it is Internet Explorer, which needs WOFF 1.0 (Chrome 6, Firefox 3.5, Safari 5.1, IE 9). If IE 11 is not on your support matrix, ship WOFF2 alone and delete the fallbacks.

How do I convert TTF to WOFF2 on a Mac?

fontTools is the reference toolchain and it is a Python package. The install is the only fiddly part on a Mac, because a bare pip install against Homebrew’s Python now refuses to run: it exits with the PEP 668 externally-managed-environment error. Two routes that work:

# Homebrew formula (fonttools 4.66.0 as of September 2026)
brew install fonttools

# or a virtualenv, which is what produced every number on this page
python3 -m venv .venv
.venv/bin/pip install fonttools brotli

Both give you the same encoder. We ran the conversion with the Homebrew-provided fontTools 4.61.1 and again inside a fresh venv, and the two WOFF2 files were byte-identical at 212,004 bytes. brotli is the part people forget: without it fontTools cannot write WOFF2 at all.

# one font
fonttools ttLib.woff2 compress -o Lato.woff2 Lato-Regular.ttf

# a folder
for f in *.ttf; do fonttools ttLib.woff2 compress -o "${f%.ttf}.woff2" "$f"; done

# and back again, when you need to inspect something
fonttools ttLib.woff2 decompress -o roundtrip.ttf Lato.woff2

Smol does it without a toolchain: drop the TTFs in, or right-click them in Finder. Its output was 211,820 bytes against fontTools’ 212,004 on the same font, a 0.09% difference, with all 3,023 glyphs and all 2,164 character mappings intact and the glyph outlines byte-identical to the TTF. Two fonts converted in under a second. It writes ttf, otf, woff and woff2, so the OTF to TTF and WOFF fallback cases are covered by the same drop.

Is subsetting a bigger win than converting to WOFF2?

Considerably bigger, and it is the part worth your attention. A full Latin text font ships thousands of glyphs: Cyrillic, Greek, Vietnamese, every currency symbol, ligature sets you will never invoke. Our fixture has 3,023 glyphs covering 2,164 characters. A marketing page in English uses perhaps ninety of them.

What you shipGlyphsWOFF2 bytesvs full WOFF2
Everything3,023212,004—
Google Fonts latin range31228,404−86.6%
Printable ASCII only11916,268−92.3%
Exactly one headline of text316,000−97.2%

From 656,568 bytes of TTF to 28,404 bytes of subset WOFF2 is a 95.7% reduction, and the Latin subset row is not an aggressive choice. It is the range Google Fonts itself serves for latin, covering accented European characters, smart quotes, dashes, the euro and the trademark sign. The ASCII row is for interfaces you fully control. The last row is for a single large display headline, where shipping thirty one glyphs as a separate face is entirely reasonable.

pyftsubset Lato-Regular.ttf \
  --unicodes="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,\
U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,\
U+2215,U+FEFF,U+FFFD" \
  --layout-features="kern,liga,clig,calt" \
  --flavor=woff2 \
  --output-file=lato-latin.woff2

# or drive it from the actual copy, for a display face
pyftsubset Display.ttf --text="Compress images on your Mac." \
  --layout-features="kern,liga" --flavor=woff2 --output-file=display.woff2

Keep --layout-features honest. Dropping kern to save a few hundred bytes will visibly damage the typography, and dropping liga breaks “fi” and “fl” in serif faces. The savings above already include those four features.

The same tool writes WOFF 1.0 with --flavor=woff, which came out at 36,264 bytes against 28,404 for the WOFF2 of the identical subset. If you still need an IE fallback, that is what it costs: 21.7% more, for one browser.

What about variable fonts?

A variable font is one file containing a continuous range of weights, so the decision is whether to ship the range or freeze a point in it.

What you shipBytesGets you
Variable TTF, as downloaded876,576Two axes, unusable as a webfont
Variable WOFF2, all axes350,460Weights 100 to 900, optical sizes 14 to 32
Variable WOFF2, latin subset61,608Same axes, Latin only
One instance frozen, WOFF2115,948Weight 400 at optical size 16, all glyphs

The interesting pair is the last two rows. Freezing a single weight and keeping every glyph cost 115,948 bytes. Keeping both axes and subsetting the character set instead cost 61,608. If your design uses more than two weights, the variable font plus a subset is smaller than the static instances would be, and it stops being a judgement call somewhere around the third weight. Instance it with fontTools.varLib.instancer if you genuinely only need one.

One note for OTF sources: the spec records that de-subroutinizing CFF outlines before WOFF2 encoding reduces the compressed size by 5–10%, at the cost of a larger intermediate file. It is an external preprocessing step, not something the encoder does for you, and it only applies to CFF-flavoured fonts rather than TrueType outlines.

How do I load the WOFF2 in CSS?

One @font-face rule per file, with font-display: swap so text paints immediately in the fallback rather than waiting on the download.

@font-face {
  font-family: "Lato";
  src: url("/fonts/lato-latin.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+20AC;
}

/* a variable font declares its range instead of a single weight */
@font-face {
  font-family: "Inter var";
  src: url("/fonts/inter-latin.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-display: swap;
}

Two details that matter more than the conversion. Preload only the one face that renders above the fold, because preloading four of them puts four requests in front of your stylesheet. And keep the unicode-range accurate: it lets the browser skip downloading a subset entirely when the page contains none of its characters, which is the whole mechanism behind Google Fonts’ per-script files.

<link rel="preload" href="/fonts/lato-latin.woff2" as="font"
      type="font/woff2" crossorigin>

crossorigin is not optional there. Fonts are fetched in CORS mode, and a preload without it downloads the file twice.

Are you allowed to convert the font at all?

Often not, and this is the part of the job that gets skipped. Converting a font to WOFF2 is creating a modified copy and publishing it on a web server. Plenty of licences forbid exactly that.

Open licences are fine and say so explicitly. Both fonts measured here are under the SIL Open Font License 1.1, which grants permission “to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software”, subject to conditions including that it may not be sold by itself and that the copyright notice and licence travel with it. Convert away, and keep the licence file next to the WOFF2.

Commercial foundry licences usually price webfont use separately. A desktop licence typically covers installing the font and making documents with it, not serving it from your domain. Many explicitly prohibit format conversion, and many require a webfont licence sold by pageview or by domain. Read the EULA you paid for. “I already bought the font” is not the same as “I may host it”.

The fonts that came with macOS are not yours to publish. They are licensed for use on Apple devices, not for redistribution from your server. Converting one to WOFF2 and shipping it is a licence violation even though nothing in the toolchain will stop you.

Check the embedding bits before you assume. Every OpenType font carries an fsType field in its OS/2 table describing what the vendor permits. It is advisory rather than enforced, and it is a fast signal:

fonttools ttx -q -t OS/2 -o - Lato-Regular.ttf | grep fsType

A value of 0 means installable embedding, 2 means restricted, 4 preview and print, 8 editable. Restricted means stop and go read the licence.

When Smol is not the answer

For a website, use pyftsubset. This is the honest headline of the whole page. Smol converts to WOFF2 and it does not subset: our output kept all 3,023 glyphs, which is 212,004 bytes where a Latin subset would have been 28,404. Since subsetting is worth roughly seven times what the format change is worth, a web build should use the tool that does both. fontTools is free, scriptable and the reference implementation.

For a build pipeline, put it in the build. A font that is converted by hand is a font that will be stale after the next update. pyftsubset in a Makefile or an npm script is versioned, reproducible and runs on CI.

For one font, one time, the one-liner is enough. No app required.

Where Smol earns the $29 is the case fontTools makes awkward: a folder of mixed TTF and OTF families that all need WOFF2 and WOFF versions, from Finder, on a machine with no Python toolchain and no permission to install one, sitting next to the images and PDFs from the same client handoff that also need processing. It matched fontTools within 0.09% on identical input, so you lose nothing on quality. One purchase, no subscription, and it runs offline.

Related: every format Smol reads and writes, PNG to WebP for the same web budget, PNG to JPG, and the rest of the asset pipeline.

Frequently asked questions

How do I convert TTF to WOFF2?

Install fontTools with brotli, then run fonttools ttLib.woff2 compress -o out.woff2 in.ttf. On a 656,568-byte TTF that produced 212,004 bytes with every glyph preserved. On a Mac, use brew install fonttools or a virtualenv: a bare pip install fails on Homebrew Python with the PEP 668 externally-managed-environment error.

How much smaller is WOFF2 than WOFF?

Measured on the same font, 212,004 bytes against 308,724, so WOFF2 was 31.3% smaller. WOFF 1.0 compresses font tables with Flate; WOFF2 uses Brotli plus a preprocessing step that transforms the glyf and loca tables first, which is where the extra margin comes from.

Is converting to WOFF2 lossless?

Yes. Decompressing a WOFF2 back to TTF produced identical glyph outlines by hash, identical hmtx metrics and an identical character map. The transformation is reversible, so the typeface is unchanged and only the file format differs.

Do I still need a WOFF 1.0 fallback?

Only for Internet Explorer. Per MDN browser compatibility data, WOFF2 is supported from Chrome 36, Edge 14, Firefox 39, Safari 10 and iOS Safari 10, and IE never supported it. On an identical Latin subset the WOFF 1.0 version was 36,264 bytes against 28,404, so the fallback costs 21.7% more.

Is subsetting more important than converting to WOFF2?

Yes, by roughly seven times. Converting the test font took it from 656,568 to 212,004 bytes. Subsetting that WOFF2 to the Google Fonts latin range took it to 28,404, another 86.6% off. Use pyftsubset with --flavor=woff2 and do both steps in one command.

Am I allowed to convert a commercial font to WOFF2?

Check the licence. Open Font License fonts explicitly permit modification and redistribution with conditions. Many commercial foundry EULAs cover desktop use only and sell webfont licences separately, and some prohibit format conversion outright. Fonts bundled with macOS are licensed for use on Apple devices, not for hosting on your server.

Keep reading