TypographyApril 2026 · 12 min read

How to Use Variable Fonts in CSS (2026)

A decade ago, using Inter on the web meant shipping eight WOFF2 files — one per weight — and accepting that any user with a slow connection would see the page flash between fallbacks as each file arrived. Today, Inter ships as one variable font that contains every weight from 100 to 900, scrubable on a continuous axis, smaller than two static files combined. The mechanics are worth learning well: variable fonts have been stable in every modern browser since 2018, the tooling has matured, and almost every serious typeface now has a variable build. Here is everything you need to ship them in 2026.

Try the Variable Font Playground
Scrub every axis, see it live, copy the CSS — registered and custom axes auto-detected
DG
Derek Giordano
Designer & Developer
⚡ Key Takeaways
  • Variable fonts pack every weight, width, slant, and optical size into a single file you scrub with CSS.
  • Why Variable Fonts Changed Type on the Web.
  • Covers anatomy of a variable font.
  • Covers registered vs custom axes.
  • Covers loading with @font-face.

Why Variable Fonts Changed Type on the Web

The old way of shipping a typeface to the web was a tax. To use Inter in regular, medium, semibold, and bold — a reasonable four-weight spread for any interface — you loaded four separate WOFF2 files, each roughly 70 to 90 kilobytes over the wire. Italic variants doubled that. If you wanted optical sizing, there was no way to do it smoothly; you picked one cut and let the browser scale it. Every extra weight was another request, another file to cache, another chance for the font to arrive late and flash the user's content.

A variable font collapses the entire weight spectrum into one file. Inter's variable build clocks in around 330 KB for all weights combined — less than two of the old static files. The savings compound when you reach for features that were prohibitive before: you can now use four weights plus italic without thinking about it. You can interpolate weight smoothly in a hover transition. You can let optical size adjust itself to the rendered pixel size, the way real printed typography has done for five hundred years. None of this was practical when every variation meant another HTTP request.

Anatomy of a Variable Font

A variable font is still an OpenType font file. Internally, it contains a master design — usually the regular weight — and a set of deltas that describe how the outlines move as you travel along each axis. The axes are declared in a table called fvar (font variations), which names each axis, gives it a four-character tag, and specifies its minimum, default, and maximum values as fixed-point numbers. The rendering engine uses this table to interpolate any value in between at render time, so you are not picking from a finite set of weights — you are asking for weight 487, and the font computes it.

💡 Tip
Always include -webkit-backdrop-filter alongside backdrop-filter for Safari support. Without the prefix, the effect is invisible to roughly 25% of mobile users.

The tag convention is the thing to remember. Registered axes — the common five — use lowercase tags: wght, wdth, slnt, ital, opsz. Custom axes use uppercase tags: GRAD, MONO, CASL, SOFT, WONK. The case is not cosmetic. It tells the spec, the browser, and the CSS engine which axes map to standardized high-level CSS properties and which require the low-level font-variation-settings escape hatch.

Registered vs Custom Axes

The five registered axes are the ones the OpenType committee agreed on often enough that they deserve their own CSS properties. wght goes from 1 to 1000 and maps to font-weight. wdth is a percentage of normal width, commonly 50–200, and maps to font-stretch. slnt is degrees of slant (zero or negative), mapped to font-style: oblique. ital is a 0 or 1 toggle for italic, also font-style. opsz is the optical size, and when font-optical-sizing is set to auto the browser picks a sensible value based on the actual rendered pixel size, no code needed.

⚠ Warning
On iOS Safari, backdrop-filter inside a position: fixed element can cause severe scroll performance issues. Test thoroughly on real iOS devices.

Custom axes are where typeface designers get interesting. GRAD on Roboto Flex changes apparent weight without changing character width — the cure for the classic "button jumps wider when you hover" bug. MONO on Recursive smoothly transitions between proportional and monospace proportions, so one typeface serves both your prose and your code blocks. CASL slides the same typeface between formal and casual voice. SOFT and WONK on Fraunces soften corners and introduce asymmetric display quirks. None of these have CSS properties — all of them require font-variation-settings.

Loading with @font-face

Loading a self-hosted variable font is almost identical to loading a static one, with one important difference: the font-weight and font-stretch descriptors take a range instead of a single value, telling the browser the file covers that whole range. This is what lets you write font-weight: 435 elsewhere and have the browser actually use it.

@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter.var.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-Italic.var.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
}

The woff2-variations format hint is the canonical way to tell the browser this is a variable font file, though modern browsers will serve the file correctly even with plain woff2. Ship it anyway; it is explicit, and it will matter for tooling that inspects the stylesheet. Notice that italic is a separate @font-face block with the same family name — italic is rarely a continuous axis in practice, and most families ship roman and italic as two files.

font-variation-settings and the Reset Gotcha

For any axis beyond the registered five, you set the value through font-variation-settings, which takes a comma-separated list of tag and value pairs:

.heading {
  font-family: 'Roboto Flex', sans-serif;
  font-weight: 700;
  font-variation-settings: 'GRAD' 100, 'opsz' 48;
}

Here is the gotcha that catches everyone at least once. font-variation-settings does not cascade additively. Every property you do not explicitly list reverts to its default. If one class sets 'slnt' -10 and another sets 'GRAD' 100, applying the second class wipes out the slant from the first. You have to repeat every axis you want to preserve in every rule that touches this property.

/* WRONG: .grade-heavy wipes out slant from .italicish */
.italicish { font-variation-settings: 'slnt' -10; }
.grade-heavy { font-variation-settings: 'GRAD' 100; }

/* RIGHT: repeat all axes you want kept */
.italicish { font-variation-settings: 'slnt' -10; }
.grade-heavy { font-variation-settings: 'slnt' -10, 'GRAD' 100; }

This is also why registered axes are the safer default whenever you can use them. font-weight and font-stretch cascade normally — weight declared in the parent rule is still inherited — and they interact sensibly with font-variation-settings. Reach for the low-level property only when you need an axis that has no registered CSS equivalent.

Pulling Variable Fonts from Google Fonts

Google Fonts' v2 API delivers variable fonts natively. The trick is spelling the request correctly — you list each axis in alphabetical order, give it a range, and the browser receives a variable WOFF2 with that range baked in. Here is Roboto Flex with four axes:



Axes are listed alphabetically in the family name, ranges are given in the same order separated by commas after the @ symbol, and individual axes use .. for the range. If you only need a subset of the axes, ask for only that subset — Google Fonts will slice the file to match, which can meaningfully reduce download size. Asking for four axes of Roboto Flex costs roughly a third of what asking for all thirteen does.

The Fallback Strategy That Works

Browser support is no longer the issue it was when variable fonts shipped in 2018. Every evergreen browser has full support: Chrome since 66, Edge since 17, Firefox since 62, Safari since 11, Opera since 53. Combined share is over 97 percent globally. The only absent cohorts are Internet Explorer, ancient Safari, and any environment that rejects WOFF2 entirely — and all three are well under one percent of real traffic today.

For the rare case you need a fallback — say, a high-traffic site where losing 1 percent of sessions to bad type matters — the progressive enhancement pattern is still the right one. Declare the static font as the default, and upgrade to the variable file inside an @supports query:

/* Static fallback for ancient browsers */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-Regular.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

/* Variable override for modern browsers */
@supports (font-variation-settings: normal) {
  @font-face {
    font-family: 'Inter';
    src: url('/fonts/Inter.var.woff2') format('woff2-variations');
    font-weight: 100 900;
    font-display: swap;
  }
}

Modern browsers hit the second @font-face block and override the first thanks to cascade order. Ancient browsers do not recognize the feature query and keep the static file. The cost of the pattern is one extra file in your build and one extra @font-face declaration — cheap insurance if you need it.

Animating Axes

This is the underused superpower. font-variation-settings is a fully animatable CSS property. You can transition it on hover, keyframe it on scroll, even tie it to a scroll-driven timeline. Because weight and grade live on the compositor with other font rendering, axis transitions run smoothly at sixty frames per second:

.link {
  font-variation-settings: 'wght' 400;
  transition: font-variation-settings 180ms ease-out;
}
.link:hover {
  font-variation-settings: 'wght' 600;
}

For layout-stable hover states — the common case where you want bolder type on hover without the button resizing — use GRAD instead of wght if the typeface has a grade axis. Grade changes apparent density without changing character advance width, so the text stays the same length, the button stays the same size, and there is no layout shift. This is the specific problem GRAD was designed to solve, and it is the reason you see it shipping in modern UI typefaces like Roboto Flex.

Five Gotchas That Will Catch You

First, the reset behavior of font-variation-settings — covered above, but worth the repeat. Every axis you omit resets to default. Always list every axis you care about.

Second, Safari and the font-weight cascade. If you declare font-weight: 100 900 in @font-face and then set font-weight: 435 on an element, Safari needs that @font-face range to match exactly what the file supports. If you declare 100 700 but the file goes to 900, Safari can refuse to honor weights above 700. Match the range to the file.

Third, font-stretch expects percentages, not keywords, when you are using it with a variable font. font-stretch: 85% works; font-stretch: condensed will clamp to the nearest keyword value and may not match what you intended.

Fourth, subsetting. Variable fonts can be subsetted to the character set your site actually uses — Latin, Latin Extended, a specific script — the same way static fonts can, and the savings are large. A Latin-only subset of Inter's variable file is around 40 KB. Tools like fonttools and glyphhanger do the job. If you are self-hosting, subset.

Fifth, font-display: swap is still the right choice for body text and the wrong choice for display type where font-loading flash is uglier than a short delay. The variable-versus-static distinction does not change this — pick the strategy that fits the use case.

When Not to Use a Variable Font

Variable fonts are the default in 2026 for any project where you control the typeface choice and care about performance — which is most of them. There are still a few cases where static is the better call. If you need a single weight of a typeface and nothing else — a brand wordmark at one size, for example — a static cut is smaller. If the typeface you want does not have a variable build, and you have already picked it for the project, wrestling it into variable is rarely worth the effort.

And if you are shipping to a locked-down corporate environment with an old browser baseline, or building for feature phones, or printing to PDF through a rendering pipeline that does not interpolate variation axes, you will want to check that the pipeline actually honors the file before committing. Outside those cases, reach for the variable build. It is smaller, more flexible, and the only future that OpenType is still investing in.

Pick a font, scrub every axis

Six curated variable fonts plus drag-and-drop upload. Custom axes auto-detected from the font file. Copy the CSS when it looks right.

⚡ Open Variable Font Playground
DG
Derek Giordano
Written by the creator of Ultimate Design Tools. BA in Business Marketing.
📚 References & Further Reading