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:
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:
fvarlists the axes, each with a tag, a minimum, a default and a maximum, and the named instances (section 8).gvarholds the point movements for TrueType outlines. CFF-flavored fonts use aCFF2table instead, which carries its own variation data.avaroptionally 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.HVARandMVARvary advance widths and font-wide metrics, andSTATdescribes 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:
| Tag | Axis | Scale in OpenType | CSS property |
|---|---|---|---|
wght | Weight | 1 to 1000; 400 is regular, 700 bold | font-weight: 650, any number from 1 to 1000 |
wdth | Width | Percent of normal width; 100 is normal | font-stretch: 87.5%, renamed font-width in CSS Fonts 4 |
opsz | Optical size | Text size in points | font-optical-sizing: auto, set from font-size (section 7) |
slnt | Slant | Degrees, −90 to 90; positive leans left | font-style: oblique 10deg |
ital | Italic | 0 (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, andWONK, which swaps in the leaningh,mandnand a curly ampersand that give the face its old-style eccentricity.WONKis 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, andMONO, from proportional to monospaced widths, so one family serves both prose and code. The full Recursive also hasslntandCRSV(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:
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: 650picks 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" 650means nothing to a static font. - They take part in matching. With several
@font-facerules in a family,font-weightandfont-stylechoose which file to use.font-variation-settingsnever 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 usefont-weight: bolder. The low-level property cannot do either, as the next section shows. - They mean something.
font-style: italicsays 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:
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:
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:
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:
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:
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:
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:
| Setting | Chrome 156 | WebKit 26.6 |
|---|---|---|
font-optical-sizing: auto | 634.59 | 634.59 |
"opsz" 24 (pixels) | 634.59 | 634.59 |
"opsz" 18 (points) | 652.11 | 652.11 |
none (default, 14) | 663.80 | 663.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:
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:
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.
| Situation | Tends to win | Why |
|---|---|---|
| One or two weights | Static | One or two small files, each downloaded only if text uses it |
| Three or more weights in use | Variable | The file costs about two statics, and every weight after that is free |
| Weights between the usual ones (450, 550), or responsive weight and width | Variable | Static files cannot do it at all |
| Regular, italic, bold, bold italic | Often static | Two variable files carry every weight for the price of four static ones or more |
| Many axes, few used | Static, or an instanced variable file | Every axis adds data; fonttools' instancer can pin unused axes, as was done for Recursive here |
| Chinese, Japanese, Korean | Variable, from the second weight | Each 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 scripts | Depends; compare | Noto 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.
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
transformandopacitycan 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-motionreports 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" 800is 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
| Concept | Key points |
|---|---|
| Design space | Masters 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. |
| History | Multiple 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 axes | wght → 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 axes | Uppercase tags, such as Fraunces's SOFT and WONK and Recursive's CASL and MONO; reachable only through font-variation-settings. |
| High-level first | The 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 trap | font-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 ranges | font-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 sizing | auto by default; Chrome and Safari's engine set opsz to the font size in CSS pixels, though OpenType defines points. |
| Instances and default | Named instances are labels for points; CSS selects by number. The default instance is what non-variable software draws. |
| File size | A weight-only variable file costs about two static weights; each axis and each italic adds more. Compare real files against the styles actually used. |
| Animation | Axes interpolate, but each frame re-lays out text; keep it short, mind width changes, honor prefers-reduced-motion. |
| For people | Avoid hairline weights for text; grade changes darkness without width; variable CJK and Arabic fonts save the most bytes, and axes should suit the script. |