TYPEFOUND

Sign in

Back to the home page

This is for the people who run the site.
The typefaces are on sale to everyone.

← The journal

Variable fonts, and when they are worth it

A variable font holds a whole design space in one file. It is bigger than one static weight and smaller than three, so the break-even is around two — and optical size is the axis worth having.

31 August 2026 · 9 min read · Perhapxin

A variable font is one file that carries a family's worth of design. It stores a single set of outlines plus instructions for deforming them, declares a set of named axes, and lets the renderer ask for any position along those axes. What comes back is an instance interpolated at that point — not a separate font that was sitting in the file waiting.

The machinery lives in the font's fvar table. Each axis has a four-letter tag, a minimum, a maximum and a default. Ask for wght 400 and you get the outlines as drawn; ask for 650 and every point moves along the deltas the designer supplied. This is why "a font that resizes" is the wrong summary — every font resizes. A variable font varies design, which is a different thing.

The five axes everything else is built around

The OpenType specification registers five design-variation axes. Registration matters because it standardises the numeric scale: a wght of 400 means Regular in any font from any foundry, which is what lets CSS map a high-level property onto an axis at all.

Tag Name Valid range "Regular" value CSS property
wght Weight 1 to 1000 400 (required) font-weight
wdth Width greater than 0, as % of normal 100 (required) font-stretch
slnt Slant greater than −90, less than +90 degrees 0 (required) font-style: oblique <angle>
ital Italic 0 to 1 0 font-style: italic
opsz Optical size greater than 0, in typographic points 10–16 recommended font-optical-sizing

Two details catch people out. First, slnt is measured in counter-clockwise degrees, so a normal right-leaning oblique has a negative slant value — the specification says so explicitly, to match the italicAngle field in the post table.

Second, ital and slnt are alternatives, not a pair. A slant axis shears the upright design; an italic axis swaps in different letterforms — a single-storey a, a cursive e. The specification says fonts should rarely use both.

Case is load-bearing too. Registered tags are lowercase; foundry-defined axes must begin with an uppercase letter. GRAD, XOPQ and YTLC in Roboto Flex are custom axes, and you can tell at a glance.

Optical size is the axis that actually improves a page

Optical size is the one most people have never set, and the only one that improves ordinary body text without anyone choosing anything. It varies stroke weight, glyph proportion and fine detail to suit the displayed size: sturdier stems and more open counters when small, finer hairlines and tighter fitting when large.

Its scale is text size in typographic points, which gives it far stronger interoperability than the other axes — two fonts set to opsz 20 are both meant for 20-point text. That is precise enough for the browser to select a value on its own, and it does.

You do not opt in to optical sizing — you opt out. font-optical-sizing: auto is the initial value, and it has been Baseline widely available since March 2020. If the font has an opsz axis, the browser is already tracking your font-size with it. The property exists mainly so you can write none and stop it.

Ranges from families that ship it: Source Serif 4 runs opsz 8 to 60, Literata 7 to 72, Newsreader 6 to 72. All three default to 14 or 16 — the text sizes the designs were drawn for.

Override it only where the automatic choice is wrong, which in practice means settings where rendered size and perceived size diverge:

.hero {
	font-size: 4rem;
	font-variation-settings: 'opsz' 60;
}

.fine-print {
	font-optical-sizing: none; /* only if auto is visibly hurting */
}

Do the arithmetic before assuming it saves bytes

A variable font is bigger than one static weight and smaller than four. Where the crossover falls depends entirely on the font, so it is worth a minute with a file listing rather than a rule of thumb.

23.1 KBone static weight
47.1 KBthe variable file
94.5 KBfour static weights
Inter 5.x, Latin subset, WOFF2, measured from the Fontsource builds — same family, same subset, same tooling. The variable file carries a weight axis only.

The full sum:

Weights needed Static total Variable Difference
1 (400) 23.1 KB 47.1 KB variable costs 2.0×
2 (400, 700) 46.9 KB 47.1 KB variable costs 236 bytes
3 (400, 600, 700) 70.8 KB 47.1 KB variable saves 33%
4 (400, 500, 600, 700) 94.5 KB 47.1 KB variable saves 50%

The break-even for this family is almost exactly two weights. At three it is a clear win; at one it is a doubling for nothing.

Two things shift the line. Italic is normally a second variable file — Inter's is 50.6 KB — so a roman-plus-italic design is two of these sums, not one. And subsetting changes the ratio, because delta data does not shrink at the same rate as outlines. Do the sum on your own files, after subsetting.

The number that matters is not the saving, it is what you would otherwise have shipped. A team genuinely going to load four weights halves its type payload. A team that loads Regular and Bold and imagined it might want more saves nothing, and pays for the imagination on every visit.

Prefer font-weight, and reach for font-variation-settings last

Declare the axis ranges in @font-face and then use ordinary properties. The range syntax in font-weight is what tells the browser this one file covers 200 through 900:

@font-face {
	font-family: 'Source Serif 4';
	src: url('/fonts/source-serif-4.subset.woff2') format('woff2');
	font-weight: 200 900;
	font-style: normal;
	font-display: swap;
}

h1 { font-weight: 720; }      /* any value in range, not just steps of 100 */
.lead { font-weight: 350; }

format('woff2-variations') is a transitional string you no longer need — Google Fonts serves its own variable files as plain format('woff2').

There are three reasons to keep font-variation-settings for custom axes only, and none of them are stylistic.

It wins the cascade unconditionally. A value set there overrides the equivalent basic property wherever the two appear in the cascade, so one stray declaration inside a component defeats every font-weight rule that should have beaten it.

It is one value, not a set of sub-properties. A rule setting 'opsz' 32 replaces an inherited 'wght' 700 rather than adding to it, and the weight snaps back to the axis default with no warning. Every axis has to be repeated in every rule that touches any of them.

And it does not survive a fallback. If the font fails to load, font-weight: 700 still produces a bold from the fallback face; font-variation-settings: 'wght' 700 means nothing to a font with no wght axis, and the text renders Regular.

Loading it

font-display: swap for display type and optional for body text, exactly as with a static face. Preload is where a variable font is genuinely easier — one file rather than four, so the advice to preload only the face in the first screenful stops being a painful choice.

<link rel="preload" href="/fonts/source-serif-4.subset.woff2" as="font" type="font/woff2" crossorigin>

crossorigin is required even for a same-origin font, or the browser fetches the file twice.

Animating the weight axis is usually a mistake

It works, and it is the first thing everyone tries. font-variation-settings is animatable, transitions interpolate it, and a heading that swells from Light to Black on hover demos well.

The trouble is mechanical. Changing weight changes glyph advance widths, so the line measures differently every frame and the browser re-breaks the text underneath you. The glyphs are re-rasterised each frame too — this is not transform, which a compositor handles without touching layout. On a slow device the cost is visible, and whether the effect earned it is a judgement call at best.

If you do it, contain it. Animate one short word inside a box with a reserved width so nothing reflows, keep the range narrow, and honour the user's setting:

.mark {
	display: inline-block;
	min-width: 6ch;                 /* reserve the widest state */
	font-variation-settings: 'wght' 400;
	transition: font-variation-settings 180ms ease;
}

.mark:hover { font-variation-settings: 'wght' 600; }

@media (prefers-reduced-motion: reduce) {
	.mark { transition: none; }
}

What a variable font cannot give you

A variable font is one designer's interpolation. The masters are drawn at the ends, with whatever intermediate masters were needed to keep the middle honest, and everything between those points is arithmetic the designer approved rather than shapes they cut. A weight of 550 is a weight the designer permitted — usually fine, and not the same claim as a weight they drew.

The harder limit sits upstream of all of it: a face with one weight has nothing to interpolate. There is no design space, no second master, no deltas — there is one set of outlines, and a variable build of it would be the same file with an empty axis.

That is the case for every typeface we make. Typefound's eight faces are each a single weight with no italic; Drip, the face setting this page, is 105 characters and one master. None of them are variable, and none of them could be.

It also explains a decision on this site that looks odd until you know why. Ask a browser for bold on a single-weight font and it will not refuse — it invents one by smearing the glyph sideways, which on a face with sharp terminals reads as a printing fault. So the poster tool grows the real outline with -webkit-text-stroke rather than asking for a weight that does not exist.

Variable fonts are an excellent delivery format for a family that already has range. They do not create range. The question to settle first is not whether to ship a variable file — it is how many weights the design genuinely has, and whether the page needs more than two of them.

More from the journal

These notes are written while drawing the typefaces this site sells. They are display faces, licensed for commercial work, from $3.