Skip to main content

· Typography · 6 min read

Variable Fonts on the Web: Axes, font-weight Ranges, and font-variation-settings

How to load a variable font with @font-face, control its axes with normal CSS properties, and use font-variation-settings only where you need it.

A variable font is one file that contains a whole range of styles. Instead of separate files for Light, Regular, Semibold, and Bold, it has a weight axis you can set to any value in between: 400, 450, or 637.

Using one well takes three things: knowing which axes the font has, declaring them correctly in @font-face, and choosing the right CSS property to control each axis. This guide covers all three.

Axes: what a variable font can change

Each way a variable font can vary is an axis, identified by a four-letter tag. The OpenType axis registry defines five standard ones, and CSS has a regular property for each, as MDN’s font-variation-settings reference shows:

Axis tagNameCSS property to use
wghtWeightfont-weight
wdthWidthfont-stretch
slntSlantfont-style: oblique <angle>
italItalicfont-style: italic
opszOptical sizefont-optical-sizing (automatic)

Fonts can also have their own axes. By convention their tags are uppercase, for example GRAD for grade in some families, while registered tags are lowercase. MDN points out that not every font follows the convention and that tags are case-sensitive, so copy them exactly.

Step 1: Find out which axes your font has

Open the font in the font viewer. For a variable font it shows:

  • the weight range, such as “100–900 (variable)”, in the font details;
  • every axis with its minimum, maximum, and default value;
  • a “Variable axes” panel next to the preview, with a slider for each axis and a list of the font’s named instances (styles such as “Semibold Condensed” that the designer defined).

As you move the sliders, “CSS for these settings” under the preview shows the CSS that reproduces what you see: font-weight for the weight axis and font-variation-settings for the others. Write down the ranges; you need them in the next step.

Step 2: Declare the ranges in @font-face

For a variable font, the font-weight, font-stretch, and font-style descriptors accept ranges. Use the range the font really supports:

@font-face {
  font-family: 'Brand Sans';
  src: url('/fonts/brand-sans-variable.woff2') format('woff2');
  font-weight: 100 900;
  font-stretch: 75% 125%; /* only if the font has a wdth axis */
  font-style: normal;
  font-display: swap;
}

“Brand Sans” and the numbers are placeholders; use your font’s name and ranges. Width axis values are percentages of normal width, so they map directly onto font-stretch percentages.

Why the range matters: without it, the browser treats the file as a single weight (400 by default). When you ask for font-weight: 700, it may draw a synthetic bold on top of the regular instance instead of using the font’s real bold. MDN also notes that some browsers only let font-variation-settings take over the weight when the @font-face rule includes a font-weight range.

Many families ship italics as a second file. Give it its own rule with the same family name:

@font-face {
  font-family: 'Brand Sans';
  src: url('/fonts/brand-sans-italic-variable.woff2') format('woff2');
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
}

If you convert a variable TTF with the TTF to WOFF2 converter, the variation data is packed into the WOFF2 file along with the rest of the font. The web font kit from the converter or the font viewer writes the weight range read from the font into its @font-face rule. The converter will not turn a variable font into TTF or OTF when that means rebuilding the outlines, because every axis would be lost; keep it as WOFF2, or fix one style first with the variable font instancer.

Step 3: Use normal CSS properties first

With the ranges declared, the regular properties do most of the work:

body {
  font-family: 'Brand Sans', system-ui, sans-serif;
  font-weight: 400;
}

h1 {
  font-weight: 780; /* any value inside the range works */
}

.compact {
  font-stretch: 85%;
}

These properties cascade and inherit like any other CSS, are easy to read, and still mean something when the fallback font is in use: a fallback without a weight axis simply picks its closest weight.

Optical size happens automatically

If a font has an opsz axis, browsers set it to match the text size by default, because font-optical-sizing starts as auto. Small text gets the sturdier shapes designed for small sizes; large headings get the finer ones. You usually do not need to touch it. To turn it off, use font-optical-sizing: none. See MDN’s font-optical-sizing page.

Step 4: Use font-variation-settings for the rest

Custom axes have no CSS property of their own. That is what font-variation-settings is for:

.headline {
  font-variation-settings: 'GRAD' 50;
}

MDN describes it as a low-level mechanism to use only when no regular property exists for an axis. Two behaviors explain why:

It overrides the regular properties. Per MDN, a value set with font-variation-settings wins over the matching regular property, such as font-weight, no matter where it appears in the cascade. Writing 'wght' 700 there means a later font-weight: 400 has no effect on that element.

Each declaration replaces the whole list. Like any CSS property, the value is not merged with values from other rules. If .headline sets 'GRAD' 50 and .headline.loud sets 'CASL' 1, the loud headline loses its grade setting.

A common way around the second problem is one custom property per axis, combined in a single declaration:

:root {
  --grad: 0;
  --casl: 0;
}

.headline {
  --grad: 50;
}

.loud {
  --casl: 1;
}

.headline,
.loud {
  font-variation-settings:
    'GRAD' var(--grad),
    'CASL' var(--casl);
}

Each rule now changes only its own axis. Axes the font does not have are ignored. Keep opsz out of the list unless you mean to fix it, or automatic optical sizing stops working for those elements.

Animating weight

Because weight is a continuous axis, you can transition it:

a {
  font-weight: 400;
  transition: font-weight 0.2s;
}

a:hover {
  font-weight: 600;
}

Heavier letters are usually wider too, so the text around the link can move. Try it in a paragraph before using it on body text, and respect visitors who ask for less motion:

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

Is a variable font smaller?

It depends on how many styles you use. One variable file replaces many static files, which saves bytes and requests if you use several weights. If you only need Regular and Bold, two static files can be smaller than one variable file that also contains every weight in between. Convert both versions to WOFF2 with the font converter and compare the sizes it shows.

If you need only part of a font’s range, FontPivot’s variable font instancer can narrow an axis or fix it at one value in your browser, and shows the real file size as you go. On the command line, fontTools’ varLib.instancer can limit an axis (wght=300:700), fix it at one value (wght=400), or drop it (wdth=drop). Check the font’s license before modifying it.

Browser support

Every current major browser supports variable fonts; Can I use has the details. A browser without support for variations draws the font’s default style instead, so keep a sensible fallback stack such as system-ui, sans-serif all the same.

Summary

  • Check the axes and ranges first; the font viewer lists them and lets you try them.
  • Declare ranges in @font-face: font-weight: 100 900, and font-stretch or font-style ranges if the font has those axes.
  • Control registered axes with font-weight, font-stretch, font-style, and automatic optical sizing.
  • Use font-variation-settings only for custom axes, and combine axes through custom properties.
  • Compare file sizes before assuming the variable version is smaller.

Further reading

Share:
Back to guides

Related guides

All guides »