CSE 134B Web Client Languages
  1. Presentation
  2. Fonts
  3. Variable fonts

Presentation · Fonts 08

Variable fonts

A static font is one style frozen in a file. A variable font is a range of styles in one file, and CSS can ask for any point in that range: weight 650, a width of 87%, the letterforms meant for 9-point captions. This page covers how that works, how to ask for it in CSS, and when it actually saves anything.

The earlier pages treated a family as a set of separate faces: regular, italic, bold, each its own file and its own @font-face rule. Variable fonts replace the set with a space. The ideas are not hard, but the CSS has a few traps, and the performance case is less automatic than it is often made to sound. One playground lets you move through four fonts' axes and see the CSS that does it.

1. One file, a design space

A type designer drawing a family does not draw every weight from scratch. They draw a few masters, say the thinnest and the heaviest, and derive the weights between by interpolation: every point on every outline moves along a straight line from its position in one master to its position in the other. For interpolation to work, the masters must be compatible: the same glyphs, each with the same number of points in the same order, so that point 17 of the light a has a partner in the bold a. For decades, interpolation happened in the designer's software, and what shipped was the result: a handful of static files at fixed points.

A variable font ships the interpolation itself. Each direction the design can vary in is an axis, the axes together make a design space, and the browser computes the outlines for whatever point in that space the CSS asks for. Inter in this section is a good small example. It has two axes, weight from 100 to 900 and optical size from 14 to 32, and its masters sit at the corners and edges of that rectangle:

Inter's design space: two axes, six masters and one instance between themA rectangle with weight on the horizontal axis, from 100 to 900, and optical size on the vertical axis, from 14 at the bottom to 32 at the top. Six masters are marked: weights 100, 400 and 900 at optical size 14, and the same three weights at optical size 32. The master at weight 400, optical size 14 is the default, whose outlines are stored whole; the other five are stored as differences from it. An instance at weight 650 and optical size 24, which no one drew, sits inside the rectangle, with dashed lines to the axes; the browser computes it by blending the masters around it.defaultmastermasterinstancewght 650, opsz 24100400650900weight (wght)142432optical size (opsz)5 sets ofdifferences+ 1 default
Inter's design space. Six masters, one of them the default; the instance at weight 650 and optical size 24 was never drawn, only computed.

The file does not hold six complete fonts. It holds one set of outlines, those of the default master, and for every other master a list of how far each point moves. To draw weight 650 at optical size 24, the renderer works out how much each master contributes at that location, scales its movements by that amount, and adds them to the default outline. The result is an instance: a point in the space, designed in the sense that its masters were, but drawn by the renderer.

That is the difference from the fake bold a browser makes when no bold face exists (faux bold and italic in Fonts in CSS). A synthetic bold smears a regular's outline outward, evenly, everywhere. An interpolated weight follows the designer's masters: the bowls of the a thicken while its counter keeps its shape, the stems grow in the proportions the designer chose. The CSS Fonts specification says so directly: font variations do not count as synthesis.

The tables involved

A font is a set of named tables (the diagram on the loading page shows the main ones), and variation adds a few:

  • fvar lists the axes, each with a tag, a minimum, a default and a maximum, and the named instances (section 8).
  • gvar holds the point movements for TrueType outlines. CFF-flavored fonts use a CFF2 table instead, which carries its own variation data.
  • avar optionally bends an axis, so equal steps in CSS give visually equal steps in the design. Inter's bends the upper half of its weight axis slightly, so the weights from 400 to 900 step evenly to the eye rather than along a straight line.
  • HVAR and MVAR vary advance widths and font-wide metrics, and STAT describes how the axes and styles relate.

None of this has to be written by hand; the point is that variation data has weight. In this section's Inter, before compression, the variations in gvar take 78,384 bytes and the default outlines in glyf only 23,680. Section 9 comes back to what that means for downloads.

2. From Multiple Master to OpenType 1.8

Variable type is older than the web font. Adobe's Multiple Master fonts of 1991 put design axes in a PostScript font, and Apple's TrueType GX variations followed in the 1990s (A short history of type tells that part). Both worked, and neither caught on: few applications exposed the axes, and the files only ran on some systems.

The idea came back in September 2016, when Apple, Google, Microsoft and Adobe jointly presented OpenType 1.8 at the ATypI conference in Warsaw, adding font variations to the format every system already read (the history page has the details). This time the four companies that make the main operating systems and browsers agreed together, and support arrived quickly: Safari 11 and Chrome 62 in 2017, Firefox 62 in 2018. A familiar idea from 1957 returned with it. Adrian Frutiger numbered Univers on a grid of weights and widths (Geometry, neutrality and the numbered family); a variable font makes that grid continuous.

3. Registered and custom axes

Every axis has a four-character tag. The OpenType specification registers five, with fixed meanings and scales, and CSS maps each one to a property that already existed:

The registered axes and the CSS that sets them
TagAxisScale in OpenTypeCSS property
wghtWeight1 to 1000; 400 is regular, 700 boldfont-weight: 650, any number from 1 to 1000
wdthWidthPercent of normal width; 100 is normalfont-stretch: 87.5%, renamed font-width in CSS Fonts 4
opszOptical sizeText size in pointsfont-optical-sizing: auto, set from font-size (section 7)
slntSlantDegrees, −90 to 90; positive leans leftfont-style: oblique 10deg
italItalic0 (roman) to 1 (italic)font-style: italic

Two details hide in that table. First, the slnt axis counts a counter-clockwise lean as positive, so ordinary forward-leaning text has a negative slant, while CSS's oblique 10deg means a forward lean. Browsers flip the sign for you: in Chrome 156 and in the WebKit engine behind Safari 26, font-style: oblique 10deg on a font with a slant axis drew exactly what "slnt" -10 drew. Second, font-width is the property's new name, but as of October 2026 Safari (since 18.4) and Firefox (since 155, by MDN's data) support it and Chrome does not, so font-stretch, which every browser reads, is still the one to write.

Font designers may also invent custom axes, written in capitals to keep them apart from the registered lowercase ones. Two fonts in this section have them:

  • Fraunces has SOFT, from 0 to 100, which rounds its corners and terminals, and WONK, which swaps in the leaning h, m and n and a curly ampersand that give the face its old-style eccentricity. WONK is really a switch: rather than blending outlines, the font substitutes different glyphs when it is on, and it also turns the wonky letters off by itself at small optical sizes, where they would look like mistakes.
  • Recursive has CASL, from a crisp "linear" style to a brush-like casual one, and MONO, from proportional to monospaced widths, so one family serves both prose and code. The full Recursive also has slnt and CRSV (cursive letterforms). This section's copy pins those two at their defaults to save bytes: the full Latin file is 304,988 bytes, the trimmed one 128,708.

Custom axes have no CSS property. The only way to reach them is font-variation-settings, the subject of section 5.

4. High-level properties first

For the registered axes, a variable font needs no new CSS at all. The properties written for static fonts simply stop being restricted to a few values:

h1 { font-family: "Fraunces", Georgia, serif; font-weight: 650; /* wght: any number from 1 to 1000 */ } .label { font-weight: 540; font-stretch: 87.5%; /* wdth, if the font has one */ } .aside { font-style: oblique 8deg; /* slnt, sign handled by the browser */ } body { font-optical-sizing: auto; /* opsz from font-size; already the default */ }

The CSS Fonts specification is blunt about which way to go: "it is preferable to use font-weight: 700 rather than font-variation-settings: "wght" 700." The reasons are practical:

  • They work with any font. font-weight: 650 picks the nearest static face when the variable font has not loaded, or never will, and the fallback in the stack still gets bold where bold was meant. "wght" 650 means nothing to a static font.
  • They take part in matching. With several @font-face rules in a family, font-weight and font-style choose which file to use. font-variation-settings never selects a font; it only adjusts the one already chosen.
  • They inherit and combine. Each property is separate, so a heading can change its weight without touching its width, and <strong> inside it can use font-weight: bolder. The low-level property cannot do either, as the next section shows.
  • They mean something. font-style: italic says the text is italic; tools, user style sheets and readers' overrides understand it.

Who wins when both are set

When several settings touch the same axis, the specification applies them in a fixed order, each later step overriding earlier ones:

The order in which variation settings are applied; later steps winFive steps from left to right, each overriding the ones before it. One: font-weight, font-width and font-style, clamped to the @font-face ranges and to the font. Two: a named instance from the @font-face rule, which no browser implements yet. Three: the font-variation-settings descriptor inside @font-face. Four: font-optical-sizing, which sets opsz. Five: the font-variation-settings property, which wins over everything. A note below: font-weight 400 with font-variation-settings "wght" 700 draws weight 700.1. font-weight,font-width,font-style(clamped)2. namedinstance(unimplemented)3. @font-facefont-variation-settings4. font-optical-sizingopsz5. propertyfont-variation-settingsearlier: overriddenlater: winsfont-weight: 400; font-variation-settings: "wght" 700;draws weight 700: the low-level property overrides the high-level one for that axis
The specification's resolution order, simplified. The font-variation-settings property comes last, so whatever axis it names, it wins.

Tested in Chrome 156, font-weight: 400 with font-variation-settings: "wght" 700 gives text exactly as wide as font-weight: 700. That is the rule to remember: name an axis in font-variation-settings and the high-level property for it stops having any effect. Mixing them for the same axis is a recipe for a heading that refuses to change weight. Note also step 3: a font-variation-settings descriptor in @font-face outranks font-weight, though only Firefox and, since version 140, Chrome support the descriptor; Safari does not.

5. font-variation-settings and its trap

font-variation-settings takes a comma-separated list of tag and value pairs, and reaches any axis the font has:

.headline { font-family: "Fraunces", Georgia, serif; font-weight: 700; font-variation-settings: "SOFT" 100, "WONK" 0; }

Tags are case-sensitive and quoted. An axis the font lacks is ignored, and a value outside the font's range is clamped to it. All of that is forgiving. The trap is in inheritance. font-variation-settings is one property whose value is the whole list, and like any inherited property (The cascade), a child either inherits the parent's value or replaces it. It cannot add one axis to the parent's list:

.headline { font-variation-settings: "SOFT" 100, "WONK" 0; } .headline em { font-variation-settings: "SOFT" 0; /* meant: only change SOFT */ }

The em's computed value is "SOFT" 0 and nothing else, so WONK returns to the font's default. For Fraunces that default is 1, and the wonky letters the headline had switched off reappear inside the emphasis. Every rule that sets the property must therefore repeat every axis it wants, which gets out of hand once a design uses three or four.

The custom-property pattern

The usual fix keeps one custom property per axis (custom properties) and writes font-variation-settings in a single rule that applies to every element involved. Children change only the custom property, and the one rule rebuilds the whole list from the inherited values:

.headline { --soft: 100; --wonk: 0; } .headline, .headline * { font-variation-settings: "SOFT" var(--soft, 0), "WONK" var(--wonk, 1); } .headline em { --soft: 0; /* WONK stays 0, inherited through --wonk */ }

Checked in Chrome: the em now computes to "SOFT" 0, "WONK" 0. The rule has to match the descendants too (.headline *), because a var() is resolved where the declaration applies; if font-variation-settings were only set on .headline, the em would inherit the already-computed list and ignore its own --soft. The defaults in var() make the rule safe on elements that set nothing. Keep registered axes out of this list: weight belongs to font-weight, which inherits on its own.

6. Ranges in @font-face

A static face is declared at one point: font-weight: 700 means "this file is the bold." A variable face covers a span, and its rule should say so with a range, two values in the same descriptor:

@font-face { font-family: "Literata"; src: url("/fonts/literata-var.woff2") format("woff2"); font-weight: 200 900; font-style: normal; font-display: swap; } @font-face { font-family: "Literata"; src: url("/fonts/literata-var-italic.woff2") format("woff2"); font-weight: 200 900; font-style: italic; font-display: swap; } /* A font with wdth and slnt axes would also declare: */ /* font-stretch: 75% 125%; font-style: oblique 0deg 12deg; */

The ranges do two jobs. For matching, they tell the browser that a request for weight 650 can be met by this file, and which of two files, roman or italic, answers font-style: italic. For clamping, the browser keeps the axis values it applies inside the declared range, then inside what the font supports.

What happens without them

It depends on what the rule says instead, and the answer has changed. In CSS Fonts 4 the initial value of these descriptors is auto: for matching, the face is treated as normal weight and style, but no clamping happens. So a rule that simply leaves font-weight out still lets the axes move. In Chrome 156 and WebKit, Inter declared with no font-weight at all drew font-weight: 700 exactly as the ranged rule did.

The harmful case is a rule copied from a static template, with a single value:

@font-face { font-family: "Inter"; src: url("/fonts/inter-var.woff2") format("woff2"); font-weight: 400; /* left over from the static regular */ }

Now the browser clamps wght to 400 and, seeing a regular face asked for bold, applies its synthetic bold on top. In Chrome 156, <strong> text in this setup had exactly the regular's width and about 9% more ink than the regular, where the real weight 700 has about 50% more; WebKit widened the letters instead. Either way the designer's bold is in the file, unused. Declare the range the font actually has, which fonttools or the font's documentation will tell you, and the problem cannot arise; it also documents the file for the next person.

Format hints

Older articles write format("woff2-variations") or, more recently, format("woff2") tech(variations), so that a browser without variation support would skip the file. Every browser in use now supports variations, so plain format("woff2") is enough. The hints are harmless where supported, but tech() arrived only in Chrome 108, Firefox 107 and Safari 17, and older browsers that do not understand it may reject the whole src.

7. Optical sizing

Metal type was cut separately for each size, and the small sizes were not just shrunken large ones. Caption type had sturdier strokes, larger x-heights, more open counters and looser spacing so it would survive small print and poor paper; display sizes had fine hairlines and tight spacing, details that would fill in at 8 points. When type became scalable outlines, one drawing served every size and those adjustments were lost. The opsz axis brings them back.

font-optical-sizing has two values. auto, the default, lets the browser set opsz from the text's font-size, clamped to the font's range. none leaves the axis at the font's default. So a font with an optical size axis adjusts itself to each size with no CSS at all, and the main reason to know the property is to recognize when it is doing something.

Points or pixels?

There is a unit mismatch underneath. OpenType defines the opsz scale as "text size, in typographic points," a point being 1/72 of an inch. A CSS pixel is 1/96 of an inch, so 16px text is 12pt, and a strict reading would set opsz to 12 for it. The CSS specification leaves the mapping to the browser, and browsers chose pixels. Measured for this page, October 2026, with the same sample string in Inter at 24px:

Width of one line of Inter at 24px, in CSS pixels, under different optical sizes
SettingChrome 156WebKit 26.6
font-optical-sizing: auto634.59634.59
"opsz" 24 (pixels)634.59634.59
"opsz" 18 (points)652.11652.11
none (default, 14)663.80663.80

auto matched "opsz" 24 to the pixel in both engines, in screenshots as well as widths, and Literata at 40px agreed. When the CSS Working Group discussed it in 2019, a Chrome engineer reported that Safari and Firefox used pixels and that Chrome had been made to match. The choice suits Apple's platforms, where a point and a CSS pixel are the same size on screen, and makes optical sizes slightly "larger" than a strict reading on Windows; an old Firefox bug report arguing for a ¾ factor there is still open, and the Working Group has discussed letting authors set the ratio. For now, 1px is 1 unit of opsz wherever this page could test it. Firefox was not tested here.

Most of the time the default is right. The cases for overriding it are a design that wants the display cut at a small size for effect, and type that will be seen from far away, such as a slide or a screen across a room, where small text needs the sturdier small cut even at a large pixel size. Both use font-variation-settings: "opsz" N, which wins over auto.

8. Named instances and the default

A variable font usually lists named instances in its fvar table: points in the design space with names, so a design application can offer "SemiBold" in a menu. This section's Inter names nine, Thin to Black, each at optical size 14; Recursive names 32, from "Mono Linear Light" to "Sans Casual ExtraBlack". Read them with a few lines of fonttools:

from fontTools.ttLib import TTFont font = TTFont("fraunces-var.woff2") for axis in font["fvar"].axes: print(axis.axisTag, axis.minValue, axis.defaultValue, axis.maxValue) print(len(font["fvar"].instances), "named instances") # opsz 9.0 9.0 144.0 # wght 100.0 900.0 900.0 # SOFT 0.0 0.0 100.0 # WONK 0.0 1.0 1.0 # 6 named instances

Browser-based inspectors such as Wakamai Fondue show the same information for a dropped-in file, along with every OpenType feature. CSS does not select instances by name. Ask for font-weight: 600 and you get the point at 600, which is where "SemiBold" sits anyway. CSS Fonts 4 defines a font-named-instance descriptor for @font-face, but MDN's compatibility data records no browser that implements it.

Why the default matters

Look again at that output: Fraunces's default weight is 900. The default instance is the one whose outlines are stored whole in glyf, and the designer chooses it. CSS hides the choice, because font-weight: normal asks for 400 and the browser moves the axis there. It shows up elsewhere. Software that reads the outlines but ignores the variation tables draws every letter at the default: Fraunces would come out Black, this section's Bitter Thin (its default is 100), Inter regular. A design that has to survive such a renderer, or a variable font used as a static one in an old tool, is safer with a font whose default is its regular.

9. One file or several

"One variable file instead of eight static ones" is the usual pitch, and it is true about requests. It is not automatically true about bytes, because a variable file carries the whole space while a static file carries one point of it, and the page pays for every byte of the file it downloads. Comparing this section's variable files with the static Latin files of the same families from Fontsource 5.3.0, all WOFF2:

Static files against variable files, in bytes, for Inter and LiterataHorizontal bars on one scale. Inter: one static weight, 23,664 bytes. Two static weights, 400 and 700, 48,020 bytes. The variable file with only a weight axis, 48,256 bytes, almost the same as two statics. The variable file with weight and optical size, 72,920 bytes. Four static weights, 400 to 700, 96,744 bytes. Literata: four static files, regular, italic, bold and bold italic, 86,472 bytes. Variable roman and italic with weight only, 106,224 bytes. Variable roman and italic with weight and optical size, 223,260 bytes, the largest bar.Inter (Latin, WOFF2)static 40023,664static 400 + 70048,020variable, wght48,256variable, wght + opsz72,920static 400, 500, 600, 70096,744Literata (Latin, WOFF2)static 400, 400i, 700, 700i86,472variable ×2, wght106,224variable ×2, wght + opsz223,260static filesvariable files (×2: roman and italic)
Bytes for the same text coverage. A weight-only variable Inter costs what two static weights cost; adding optical size costs a third. Literata's four classic styles are cheaper as statics.

Read across the bars and a rule of thumb appears. A weight-only variable file costs about as much as two static weights, so it wins from the third weight on. Each extra axis adds to the price: Inter's optical size raises the file by half, and Literata's more than doubles it. And italics are a second variable file, so a family that needs only regular, italic, bold and bold italic, which describes most text on most sites, is often cheaper as four statics.

When one variable file tends to win, and when static files do
SituationTends to winWhy
One or two weightsStaticOne or two small files, each downloaded only if text uses it
Three or more weights in useVariableThe file costs about two statics, and every weight after that is free
Weights between the usual ones (450, 550), or responsive weight and widthVariableStatic files cannot do it at all
Regular, italic, bold, bold italicOften staticTwo variable files carry every weight for the price of four static ones or more
Many axes, few usedStatic, or an instanced variable fileEvery axis adds data; fonttools' instancer can pin unused axes, as was done for Recursive here
Chinese, Japanese, KoreanVariable, from the second weightEach static weight repeats tens of thousands of glyphs; Noto Sans SC's variable file costs less than two of its seven static weights (section 12)
Arabic and other mid-sized scriptsDepends; compareNoto Sans Arabic's variable file costs about three and a half static weights, so it wins from the fourth (section 12)

Bytes are not the only cost. Static weights are downloaded lazily, each when text first needs it, so a page that uses bold only below the fold may never fetch it, while a variable file arrives whole before any weight can be drawn. On the other hand, one file means one request, one preload and one swap, with no stage where regular has arrived and bold is still a fallback. This site sets its own type in eight static Plex faces and weighs that trade-off in Web fonts and performance; its font budget applies to variable files unchanged. The habit that settles it: list the styles the design actually uses, then compare the real files.

10. Animating axes

Because every point in the space exists, axes can be animated and transitioned like any number. font-weight interpolates as a number. font-variation-settings interpolates value by value when the two lists name the same axes, in any order; if they do not, the specification says the value simply jumps from one to the other partway through.

nav a { font-weight: 450; transition: font-weight 0.2s ease-out; &:hover, &:focus-visible { font-weight: 600; } } @media (prefers-reduced-motion: reduce) { nav a { transition: none; } }

Three cautions come with it:

  • It costs layout. Changing an axis changes the shapes of the glyphs and usually their widths, so the browser must reshape the text, lay it out again and repaint it on every frame, on the main thread. Animations of transform and opacity can run off the main thread; font axes cannot. A short transition on a few words is cheap; a looping animation of a paragraph is not.
  • Width changes move things. A link that gets bolder gets wider, and pushes its neighbors along the line or onto the next one. Give such elements room, or use an axis that keeps width constant (section 12).
  • Motion is not free for readers. Moving text distracts, and for people with vestibular disorders large or looping motion can cause real discomfort. prefers-reduced-motion reports the reader's system setting; honor it by removing the animation, or reducing it to an instant change, as the sample does. Accessibility treats motion as one of the qualities of a site.

Used with restraint, animated axes have honest uses: a weight that settles as a heading comes into view, a button that firms up when pressed, a loading state that breathes. Used freely, they are the 2020s' version of blinking text.

11. Support and fallbacks

Variable fonts are Baseline widely available: font-variation-settings has worked in Chrome and Edge since 2017 and 2018, Firefox since 62 and Safari since 11, and web-features marks it widely available since March 2021. font-optical-sizing followed in Safari 13.1 and Chrome 79 and has been widely available since 2022. In practice, every browser that can load a WOFF2 file can vary it.

Fallbacks still matter, for the same reasons as with any web font. The file may fail or arrive late; the reader may block web fonts or override them; an old or unusual renderer, such as an e-mail client, a PDF tool or an embedded browser, may read the outlines but not the variations. The defenses are the ones from earlier pages, plus one:

  • A stack ending in a generic family, as always.
  • High-level properties, so the fallback font still gets bold where bold was meant (section 4). A heading styled only with "wght" 800 is regular in every font but the variable one.
  • A sensible default instance, so a renderer that ignores variations draws the regular rather than the Black (section 8).
  • A matched fallback computed at the weight and optical size the text actually uses, since both change widths (layout shift and matched fallbacks).

There is no longer much reason to serve static files to some browsers and variable files to others. If a design must survive environments without variation support, that is an argument for a static font, not for a second set of files.

12. Variable fonts for people

Weight, contrast and grade

Every weight is now available, including the very light ones, and they are tempting: weight 200 in a large size looks refined on a designer's high-resolution display. Thin strokes have less ink, so they read as lower contrast than the same color in a heavier weight, and on a low-resolution screen, at small sizes or for readers with low vision, hairline type breaks up. WCAG's contrast ratio is computed from colors alone; it relaxes its threshold for large or bold text but does not penalize light weights. Treat very light weights as display type, keep body text near regular, and take extra care with light text on dark, where strokes appear to spread and thin weights fill in (knockout text in Choosing and pairing).

Some variable fonts offer a better tool for that last problem: a grade axis, tagged GRAD. Google's axis registry defines it as changing the style "from lighter to bolder in typographic color, without any changes overall width, line breaks or page layout." Weight changes width; grade does not. That makes grade the axis to adjust for dark mode, for a reader who prefers more contrast (prefers-contrast: more), or for a hover effect that must not move its neighbors. Roboto Flex is a well-known family with a grade axis among a dozen others. GRAD is not one of the five registered axes, so it is set with font-variation-settings, ideally through the custom-property pattern of section 5.

Large scripts

Variable fonts help most where fonts are largest. A Chinese font holds tens of thousands of glyphs, and each static weight repeats every one of them. The Noto Sans CJK project's Simplified Chinese fonts make the point with real files: seven static OpenType weights, Thin to Black, total 58,275,516 bytes, while the single variable file covering the same range is 15,054,748. A site that needs two weights of Chinese text saves megabytes, and gets every weight in between. Arabic shows the same trend at a smaller scale: Fontsource's static Noto Sans Arabic, whole Arabic block, is 48,840 bytes at 400 and 50,228 at 700, and this section's variable file with all weights from 100 to 900 is 165,960, cheaper from the fourth weight on. (A short history of type explains why such fonts are large, and Noto why they exist.)

Two cautions keep this honest. Even a variable CJK font is far too large to send whole; on the web it is split into many small files by unicode-range (Web fonts and performance), and a variable font can be split that way as well as a static one. And some axes do not translate across scripts: Chinese and Japanese type has no italic tradition, and a slant designed for Latin letters says nothing to their readers. Axes, like typefaces, should be chosen for the script being set, which is a theme of Type for people, page 10 of this section, and of CSS for people.

13. Summary

Variable fonts, in brief
ConceptKey points
Design spaceMasters at points along axes; the file stores the default outlines plus the differences (gvar or CFF2), the axes in fvar, axis curves in avar. Instances are interpolated, not synthesized.
HistoryMultiple Master (1991) and TrueType GX came first; OpenType 1.8 (September 2016) made variations part of the common format, with support in every major browser by 2018.
Registered axeswght → font-weight; wdth → font-stretch (font-width not yet in Chrome); opsz → font-optical-sizing; slnt → font-style: oblique (sign flipped); ital → font-style: italic.
Custom axesUppercase tags, such as Fraunces's SOFT and WONK and Recursive's CASL and MONO; reachable only through font-variation-settings.
High-level firstThe ordinary properties work with fallbacks, take part in matching, inherit separately and carry meaning. font-variation-settings wins over them for any axis it names.
The inheritance trapfont-variation-settings is one list; a child that sets one axis resets the rest. Keep one custom property per axis and one rule that writes the list, matching descendants too.
@font-face rangesfont-weight: 100 900, font-stretch: 75% 125%, font-style: oblique 0deg 12deg. Omitted means auto, no clamping; a single value left from a static template clamps the axis and brings back fake bold.
Optical sizingauto by default; Chrome and Safari's engine set opsz to the font size in CSS pixels, though OpenType defines points.
Instances and defaultNamed instances are labels for points; CSS selects by number. The default instance is what non-variable software draws.
File sizeA weight-only variable file costs about two static weights; each axis and each italic adds more. Compare real files against the styles actually used.
AnimationAxes interpolate, but each frame re-lays out text; keep it short, mind width changes, honor prefers-reduced-motion.
For peopleAvoid hairline weights for text; grade changes darkness without width; variable CJK and Arabic fonts save the most bytes, and axes should suit the script.