One file, every style

For years, web developers have faced a trade-off: richer typography means more font files, and more font files mean slower pages. Traditional web fonts require a separate file for every style. Loading just Regular, Bold, and their italics can easily cost 500 KB or more in font data, before you even consider the rendering and fallback issues that come with multiple files. As a result, many developers avoid using the wide range of styles that type families offer—like Thin to Black weights, narrow or wide widths, and optical sizes—even though those styles could substantially improve the reading experience.

Variable fonts remove that constraint by packing multiple styles into a single file. This is possible through a new font specification that can significantly reduce font file sizes. As of May 2020, most browsers support variable fonts.

Fonts, typefaces, styles, and families

To understand how variable fonts work, it helps to clarify some terminology. A typeface is the underlying visual design; a font is a specific implementation of that typeface in a digital file format. A style refers to a single, specific typeface variant, such as Bold Italic, while a family is the complete set of styles a typeface offers.

In the past, each style in a family required its own font file. Variable fonts change this by containing all the styles within one file, eliminating a significant source of layout complexity and bandwidth overhead.

A specimen composition and list of different styles of the Roboto family.
Left: a specimen of the Roboto typeface family. Right: named styles within the family.

The anatomy of a variable font

Variable fonts work by defining a central, default style—usually the "Regular" design—and connecting it to other styles across a continuous range, known as an axis. The most common axis is Weight, which links the default to a Bold or Black style. Any individual style on that axis is an "instance" of the variable font, and font developers can give these instances names (for example, a Weight axis location of 600 might be called SemiBold).

For example, the variable font Roboto Flex has three styles along its Weight axis, with Regular in the center and lighter and heavier designs at opposite ends. Between these extremes, you can select from 900 distinct instances:

The letter 'A' shown in different weights.
Illustrated anatomy of the Weight axis for the typeface Roboto.

Roboto Flex also offers a Width axis, with Regular at its center and narrower and wider styles at each end. Since all axes in a variable font share the same default style, these width variations combine with the Weight axis to provide every weight with every width.

Roboto Flex in random combinations of Width and Weight

That means thousands of available styles. While that may seem excessive, this diversity of type styles can genuinely enhance the reading experience—and because it no longer costs additional file downloads, developers can choose to use as many or as few styles as their design calls for.

Italics and glyph substitution

The handling of italics in variable fonts takes one of two approaches, depending on the typeface. Some typefaces, like Helvetica or Roboto, have contours that are interpolation-compatible between Roman and Italic designs. For these, a Slant axis can be used to interpolate between the two.

Other faces—such as Garamond, Baskerville, or Bodoni—have Roman and Italic glyphs that are not interpolation compatible. The contours of a lowercase "n" in Roman, for example, don't resemble the ones used in the Italic version. For these typefaces, an Italic axis toggles between the two designs rather than interpolating through intermediate shapes.

Example of the Weight Axes for the typeface Amstelvar.
Amstelvar's "n" contours in Italic (12 point, regular weight, normal width), and in Roman. Image supplied by David Berlow, type designer and typographer at Font Bureau.

The same glyph-substitution technique can be applied for individual letters anywhere in the design space. Consider a dollar sign with two vertical bars: legible at large sizes, but murky when rendered at point sizes with too few pixels. Type designers can have the font substitute a single-bar design at smaller optical sizes, along the Optical Size axis, at a threshold they decide.

Axis definitions and ranges

There are five registered axes that control predictable, well-known features: weight, width, optical size, slant, and italics. Fonts can also define custom axes to control any design aspect the type designer chooses—serif size, swash length, ascender height, or the shape of the dot on the lowercase "i."

Even the same registered axis can use different value ranges across fonts. Both Oswald and Hepta Slab, for instance, expose a single Weight axis, but Oswald's range spans 200–700, while Hepta Slab goes from an extreme hairline weight at 1 up to 900.

The five registered axes have 4-character lowercase tags used to set their values in CSS:

Axis names and CSS values
Weight wght
Width wdth
Slant slnt
Optical Size opsz
Italics ital

Because axis availability and ranges are defined by each font's developer, it is essential to check what each specific font offers. The font's documentation should state this, or you can inspect the font with a tool like Wakamai Fondue.

Applying variable fonts in practice

Selecting axis values ultimately comes down to typographic best practices and personal taste. Like any new capability, variable fonts carry a risk of misuse. While experimenting with axes can yield striking results for titles, applying those same explorations to body text can injure legibility. Careful application is what turns capability into craft.

Expressive possibilities

Designers are already pushing the possibilities of variable fonts. Mandy Michael's work with the typeface Decovar offers a lively example of artistic expression through variable font axes.

Grass example by Mandy Michael
View this example om CodePen.

The same axes that create static variety can also drive animation. A demonstration with the typeface Zycon shows multiple axes animated together; you can see the live animation on Axis Praxis.

Typeface Zycon, designed for animation by David Berlow, type designer and typographer at Font Bureau.

Anicons is another experimental project that combines two frontier font technologies: variable fonts and color fonts, resulting in what its creators call the world's first animated color icon font, built from Material Design Icons.

A few examples of hover animations from Anicon's color icon font

At a more subtle level, "parametric axes" allow refinements that would be invisible to all but the trained eye. Typefaces like Roboto Flex and Amstelvar expose axes that deconstruct letters into four fundamental aspects of form: the positive (black) shapes, the negative (white) shapes, and the x and y dimensions. Much like mixing a primary color into another, these four aspects can fine-tune any other axis. The XTRA axis in Amstelvar, for instance, adjusts the "white" value per mille—a property you could use to even out word widths across a line by nudging the axis in opposite directions.

Amstelvar using little bits of XTRA in opposite directions so the words' widths are evened out

Using Variable Fonts in CSS

Loading Variable Font Files

Variable fonts are loaded with the same @font-face mechanism as static web fonts, with two key differences. First, you add both the new and the deprecated syntax for format and tech declarations. The future syntax, format('woff2') tech('variations'), is used by browsers that support it, while the fallback syntax for browsers with current support is format('woff2-variations'). Both declarations point to the same file.

@font-face {
    font-family: 'Roboto Flex';
    src: url('RobotoFlex-VF.woff2') format('woff2-variations');
    src: url('RobotoFlex-VF.woff2') format('woff2') tech('variations');
    font-weight: 100 1000;
    font-stretch: 25% 151%;
}

Second, instead of declaring a single weight or width, you supply the range of supported values. For instance, Roboto Flex supports a Weight axis from 100 to 1000, which maps directly to the font-weight property. By providing this range in @font-face, any value outside it is "capped" to the nearest valid value. The Width axis works similarly for font-stretch. If you use the Google Fonts API, this is all handled automatically, including static fallback fonts for browsers without variable font support.

Setting Weights and Widths

Currently, the wght axis is set via font-weight, and the wdth axis via font-stretch. With static fonts, you could only use keyword or step values (100 to 900 in increments of 100). With variable fonts, you can specify any value within the font's range:

.kinda-light {
  font-weight: 125;
}

.super-heavy {
  font-weight: 1000;
}
Roboto Flex' Weight axis being changed from its minimum to its maximum.

Similarly, font-stretch takes keywords like condensed or percentage values:

.kinda-narrow {
  font-stretch: 33.3%;
}

.super-wide {
  font-stretch: 151%;
}
Roboto Flex' Width axis being changed from its minimum to its maximum.

Italics, Obliques, and Custom Slants

The ital axis is an on/off switch for fonts that contain both regular and italic styles. The value 0 shows the regular style, 1 shows the italic, and there is no in-between state. The slnt axis is different: it is not a separate style but slants the upright letterforms. For Roboto Flex, the default value is 0, with a maximum slant of -10 degrees to lean letters rightward.

Ideally, these would be controlled via the font-style property, but as of April 2020 that approach is still being worked out by the CSS Working Group. For now, you should treat them as custom axes and set them with font-variation-settings:

i, em, .italic {
    /* Should be font-style: italic; */
    font-variation-settings: 'ital' 1;
}

.slanted {
    /* Should be font-style: oblique 10deg; */
    font-variation-settings: 'slnt' 10;
}
Roboto Flex' Slant axis being changed from its minimum to its maximum.

Optical Sizing

Typefaces can adapt their letterforms based on display size. A small footnote benefits from fewer fine details, while a large headline can handle thinner strokes and more intricate shapes. The font-optical-sizing property controls this behavior; its default value, auto, lets the browser choose an optimal size based on the font-size. You can set it to none to disable this automatic behavior.

The letter 'a' shown at different optical sizes.
The letter 'a' in Roboto Flex at different pixel sizes, then scaled to be the same size, shows the differences in design. Try it yourself on Codepen

You can also set a custom value for the opsz axis to deliberately mismatch the display size. The following CSS displays text at a large size but renders it with an optical size appropriate for 8pt:

.small-yet-large {
  font-size: 100px;
  font-variation-settings: 'opsz' 8;
}

Custom Axes

Custom axes are not mapped to existing CSS properties, so they are always set via font-variation-settings. Their tags use uppercase letters to differentiate them from registered axes. Roboto Flex provides several custom axes, the most important of which is Grade (GRAD). A Grade axis alters the apparent weight without changing the widths, so line breaks remain the same. In Roboto Flex, its range is -200 to 150, and you address it like so:

.grade-light {
    font-variation-settings: `GRAD` -200;
}

.grade-normal {
    font-variation-settings: `GRAD` 0;
}

.grade-heavy {
    font-variation-settings: `GRAD` 150;
}
Roboto Flex' Grade axis being changed from its minimum to its maximum.

Getting Variable Fonts from Google Fonts

Google Fonts now has a variable fonts catalog, and the API's default interface lets you select individual instances to include in a <link> element. To access the full range of axes or the entire range of values, you must compose the API URL manually, using the axes and values listed in the overview. The Google Variable Fonts Links tool can also generate the latest URLs for full variable font access.

Variable Font Inheritance Gotcha

Because not all axes are yet mapped to CSS properties, you may need to rely on font-variation-settings for custom axes or as a fallback. However, there is a trap: any setting you don't explicitly include in font-variation-settings resets to its default. Previous values are not inherited:

<span class="slanted grade-light">
    I should be slanted and have a light grade
</span>

In this case, the browser applies the 'slnt' 10 value from the .slanted class, then the 'GRAD' -200 value from the .grade-light class resets slnt back to the default of 0. The result is light grade text without the slant.

The workaround is to use CSS variables, since they do cascade:

/* Set the default values */
:root {
    --slnt: 0;
    --GRAD: 0;
}

/* Change value for these elements and their children */
.slanted {
    --slnt: 10;
}

.grade-light {
    --grad: -200;
}

.grade-normal {
    --grad: 0;
}

.grade-heavy {
    --grad: 150;
}

/* Apply whatever value is kept in the CSS variables */
.slanted,
.grade-light,
.grade-normal,
.grade-heavy {
    font-variation-settings: 'slnt' var(--slnt), 'GRAD' var(--GRAD);
}

An element that inherits a particular slnt value will keep it, even if you then set GRAD to something else. See "Fixing variable font inheritance" for a deeper look at this technique.

Note that animating CSS variables is not supported by design. Animations involving variable axes must operate directly on font-variation-settings:

@keyframes width-animation {
   from { --wdth: 25; }
   to   { --wdth: 151; }
}

Performance Considerations

OpenType variable fonts can pack many variations of a type family into a single file. In a Monotype experiment, 48 individual font files covering eight weights, three widths, and both the Italic and Roman styles were combined into one variable font, a file size reduction of 88%.

Still, replacing a single static font that you only use in one weight with a multi-axis variable font may not result in a net file size gain. The benefits depend heavily on your use case.

Animating fonts can cause performance issues while browser support matures. A helpful trick from the type foundry Dinamo pauses animations on elements with class vf-animation when they leave the viewport:

var observer = new IntersectionObserver(function(entries, observer) {
  entries.forEach(function(entry) {
    // Pause/Play the animation
    if (entry.isIntersecting) entry.target.style.animationPlayState = "running"
    else entry.target.style.animationPlayState = "paused"
  });
});

var variableTexts = document.querySelectorAll(".vf-animation");
variableTexts.forEach(function(el) { observer.observe(el); });

For fonts reacting to user input, you should throttle or debounce those input events to avoid rendering nearly identical instances of the font.

If you rely on Google Fonts, preconnecting to https://fonts.gstatic.com speeds up font delivery:

<link rel="preconnect" href="https://fonts.gstatic.com" />

This tip applies to other CDNs as well — the earlier a browser opens a network connection, the faster your fonts will be downloaded.

Browser Support and Fallbacks

All modern browsers support variable fonts. To support older ones, build with static fonts and treat variable fonts as a progressive enhancement:

/* Set up Roboto for old browsers, only regular + bold */
@supports not (font-variation-settings: normal) {
  @font-face {
    font-family: Roboto;
    src: url('Roboto-Regular.woff2');
    font-weight: normal;
  }

  @font-face {
    font-family: Roboto;
    src: url('Roboto-Bold.woff2');
    font-weight: bold;
  }

  body {
    font-family: Roboto;
  }

  .super-bold {
    font-weight: bold;
  }
}

/* Set up Roboto for modern browsers, all weights */
@supports (font-variation-settings: normal) {
  @font-face {
    font-family: 'Roboto';
    src: url('RobotoFlex-VF.woff2') format('woff2 supports variations'),
         url('RobotoFlex-VF.woff2') format('woff2-variations');
    font-weight: 100 1000;
    font-stretch: 25% 151%;
  }

  .super-bold {
    font-weight: 1000;
  }
}

In older browsers, text with the class .super-bold renders in the regular bold, because that is the only bold font loaded. In browsers that recognize the @supports rule and variable fonts, the text gets the heaviest weight of 1000. Internet Explorer does not support @supports, so it would show no styling at all, but you can use established hacks if that is an issue.

The Google Fonts API also handles fallbacks automatically. For example, requesting the Oswald font with a weight range of 200 to 700 via the API:

<link href="https://fonts.googleapis.com/css2?family=Oswald:[email protected]&display=swap" rel="stylesheet">

Browsers with variable font support get a single variable font file with all weights available from 200 to 700. Older browsers receive six static font files — one for each weight in the specified range.