FontPeeker

Variable fonts explained: axes, instances and why one file replaces twelve

6 min read · Updated June 2026

Rows of wooden letterpress type lying in a case — one block for every single size and weight.
Rows of wooden letterpress type lying in a case — one block for every single size and weight.

For most of digital typography's history a family was a pile of separate files: one for Regular, one for Bold, one for each italic, one for each condensed width. Variable fonts, added to the OpenType specification in version 1.8 in 2016, collapse that pile into a single file that can be asked for any point in between. The idea is old — Adobe's Multiple Master fonts did it in 1991 and were quietly discontinued — but this time browsers implemented it, and a variable font is now an ordinary thing to ship. Understanding what is actually inside the file makes the CSS a lot less mysterious.

What the fvar table adds to an ordinary font

A variable font is a normal sfnt font — the same glyf or CFF2 outlines, the same cmap, the same GSUB — plus a handful of extra tables. fvar declares the axes: their four-letter tags, their minimum, default and maximum values, and a name for each. gvar (for TrueType outlines) stores deltas: for each axis, how far each point of each outline should move as you travel along it. avar can warp the axis so that the midpoint of the slider is not the arithmetic midpoint of the design. STAT describes how the axis values map onto style names.

The rendering engine does the interpolation at draw time. That is why a variable file is much smaller than the sum of the static instances it replaces: the outlines are stored once and the deltas are small numbers. It is also why the design space is continuous — nothing is precomputed, so wght 437 is as legitimate a request as wght 400.

Open any variable font in FontPeeker and the Axes tab lists exactly what fvar declares, with the real minimum and maximum. If that tab says the font is static, no amount of CSS will make it interpolate.

Five registered axes, and everything else

Five axis tags are registered in the OpenType specification and are written in lower case: wght (weight, 1–1000, where 400 is regular and 700 is bold), wdth (width as a percentage of normal, where 100 is normal), opsz (optical size in points), slnt (slant in degrees, negative for a forward lean) and ital (italic, usually a 0-to-1 switch rather than a continuous range). Registered axes matter because CSS maps ordinary properties onto them: font-weight drives wght, font-width drives wdth, font-style: oblique drives slnt, and the browser sets opsz from the font size by itself.

Anything else is a custom axis and its tag is written in capitals: GRAD for grade (changes apparent weight without changing line length), YOPQ and XOPQ for the thickness of vertical and horizontal strokes in Roboto Flex, CASL for casualness in Recursive, MONO for how monospaced Recursive becomes. Custom axes have no CSS property of their own; the only way to reach them is font-variation-settings.

Named instances are the font menu

A continuous design space is useless to a font menu, which needs a list. That list is the named instances, also stored in fvar: a set of coordinates plus a name the designer chose — ExtraLight at wght 200, SemiBold at wght 600, Condensed Bold at wdth 75 and wght 700. These are the points the designer has actually looked at and approved, and they are what Word, InDesign and the macOS font panel will show you.

On the web you are not limited to them, and that is the point — but they are still the safest values to start from. A weight halfway between two named instances is legitimate, yet if the designer only checked the named ones, that is where hinting and spacing have had the most attention. FontPeeker lists every named instance as a button so you can compare an in-between value against the designer's own.

Writing the CSS: properties first, settings second

Declare the range in @font-face, not a single value: font-weight: 200 800 tells the browser that this one file answers every weight in that range, so font-weight: 500 on a paragraph resolves to wght 500 with no extra code. Do the same for width with font-width: 75% 125%, and for slant with font-style: oblique -12deg 0deg. Getting this right is what makes a variable font behave like a normal family everywhere else in your stylesheet.

Reach for font-variation-settings only for custom axes, or for a value no property can express. It is a low-level property: it does not inherit gracefully, it does not animate against font-weight, and if you set it you take over from the browser. In particular, writing font-variation-settings: "opsz" 14 freezes optical sizing — the same effect as font-optical-sizing: none — so headings will keep the text-size design. Set only the axes you actually mean to control.

When one file is not the cheaper option

A variable font is smaller than the family it replaces, not smaller than the two weights you were actually using. A typical variable text face with a weight axis and Latin plus Cyrillic coverage lands somewhere around 100–200 KB as WOFF2; two static instances of the same face might be 30 KB each. If your design uses Regular and Bold and nothing else, the static pair usually wins on bytes.

The variable file wins when you use three or more weights, when you animate or interpolate weight, when optical sizing matters, or when you want one file to cover both the UI and the editorial parts of a product. It also wins on requests and on cache behaviour: one file, one round trip, one revalidation.

Whichever you ship, subset it. A Latin-plus-Cyrillic subset of a variable face is often a third of the full file, and unicode-range in @font-face lets you split the subsets so a page that never sets Cyrillic never downloads it.

What happens when it does not work

Every browser that matters has supported variable fonts since 2018, so the realistic failure modes are not about support. They are: the font arrives but the @font-face declares a single font-weight, so the browser clamps everything to one value; a custom axis is set with lower-case tags and silently ignored; or font-variation-settings is inherited from a parent and quietly overrides the weight you set on the child.

When the font fails to load entirely, the fallback is a static system face, and a layout that relied on wght 250 for its lightness will suddenly look heavy. Pick fallbacks with a similar x-height and set font-weight explicitly on the elements that matter, so the unstyled state is merely plainer rather than broken.

The takeaway

A variable font is one file plus a table of deltas. Read fvar to learn which axes exist and what their real ranges are, declare those ranges in @font-face so ordinary CSS properties keep working, use font-variation-settings only for custom axes, and choose the variable file when you genuinely use more than two weights.

Related tool
FontPeeker · Font inspector

Open a font file and read what is inside it