Type something to search...
How to Use Variable Fonts in CSS?

How to Use Variable Fonts in CSS?

You download a variable font, drop it into your project, write font-weight: 650 on your headings, and nothing happens. Or worse, the headings render in a smeared, synthesized bold that looks nothing like the specimen. Then you add font-variation-settings: "wght" 650 and it works, until you set a grade axis on a parent and every child loses its weight. Variable fonts are simple in principle, but the CSS has a few rules that are easy to get wrong, and most of the bugs come from the same three or four mistakes.

This article walks through using variable fonts in CSS from the ground up: declaring them correctly with @font-face, controlling registered axes with standard properties, using font-variation-settings for custom axes without breaking inheritance, loading from Google Fonts or self-hosting, and wiring them into Tailwind CSS v4 and WordPress. If you want the background on what variable fonts are and when to use them, start with what variable fonts are and why you should use them.

Step 1: Declare the Font with Ranges

The single most important difference from static fonts is in @font-face. For a static font, font-weight: 700 in the declaration says "this file is the 700 weight." For a variable font, you give a range that says "this file covers every weight from X to Y."

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

Without the range, the browser assumes the file only provides the default weight (usually 400). When you ask for 700, it either picks the closest match and renders 400, or synthesizes a fake bold by smearing the outlines. That is the "my weights do not work" bug in almost every case.

The format hint

  • format("woff2") tech(variations) is the current syntax from CSS Fonts Level 4.
  • format("woff2-variations") is the older syntax that some browsers still recognize.

Listing both, as above, is the most robust approach. In practice every current browser accepts plain format("woff2") for a variable file as well, so if you prefer a shorter rule, src: url("/fonts/inter-var.woff2") format("woff2"); works.

Width and slant ranges

If the font has width or slant axes, declare those ranges too:

@font-face {
  font-family: "Roboto Flex";
  src: url("/fonts/roboto-flex-var.woff2") format("woff2");
  font-weight: 100 1000;
  font-stretch: 25% 151%;
  font-style: oblique 0deg 10deg;
  font-display: swap;
}

font-stretch takes a percentage range for the wdth axis. font-style: oblique 0deg 10deg declares the slant range so the browser uses the real slnt axis instead of a synthetic slant.

Separate italic files

Many families ship upright and italic as two variable files. Declare both under the same family name with different font-style values:

@font-face {
  font-family: "Source Serif";
  src: url("/fonts/source-serif-var.woff2") format("woff2");
  font-weight: 200 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Source Serif";
  src: url("/fonts/source-serif-italic-var.woff2") format("woff2");
  font-weight: 200 900;
  font-style: italic;
  font-display: swap;
}

Now em and font-style: italic select the italic file automatically, at any weight.

Step 2: Control Registered Axes with Standard Properties

For the five registered axes, use the normal CSS properties. They are inherited properly, they cascade the way you expect, and they also apply sensibly to fallback fonts.

AxisPropertyExample
wghtfont-weightfont-weight: 650;
wdthfont-stretchfont-stretch: 87.5%;
slntfont-stylefont-style: oblique 8deg;
italfont-stylefont-style: italic;
opszfont-optical-sizingfont-optical-sizing: auto;
body {
  font-family: "Roboto Flex", system-ui, sans-serif;
  font-weight: 420;
}

h1 {
  font-weight: 780;
  font-stretch: 85%;
}

strong {
  font-weight: 650;
}

.caption {
  font-style: oblique 6deg;
}

Any number in the declared range works, not just multiples of 100. Note that a CSS oblique angle of 6deg maps to a slnt value of -6; the browser handles the sign.

Optical size is automatic

font-optical-sizing defaults to auto, which sets the opsz axis to match the computed font size in CSS pixels. A 14px caption gets the small-size design and a 64px headline gets the display design without you writing anything. You only need to intervene to turn it off or override it:

.logo-text {
  font-optical-sizing: none;
}

.hero-title {
  font-variation-settings: "opsz" 144;
}

For why this matters for screen readability, see what optical sizing is in typography.

font-stretch and font-width

CSS Fonts Level 4 renamed font-stretch to font-width and kept font-stretch as an alias. Use font-stretch for now, because it has universal support.

Step 3: Use font-variation-settings for Custom Axes

Custom axes, written in uppercase such as GRAD, XTRA, CASL, or MONO, have no dedicated property. You set them with font-variation-settings, a comma-separated list of axis tags and values:

.button {
  font-variation-settings: "GRAD" 100;
}

.code {
  font-family: "Recursive", ui-monospace, monospace;
  font-variation-settings: "MONO" 1, "CASL" 0;
}

Axis tags are case-sensitive and must be four characters in quotes. Values outside the font's range are clamped.

Avoid it for registered axes

You can write font-variation-settings: "wght" 650, but you should not. It overrides font-weight at a low level, so:

  • The browser no longer knows the "real" weight, so fallback fonts render at 400 while the web font loads.
  • font-weight: bold on a child will not change anything, because font-variation-settings wins.
  • strong and b stop working as expected.

Keep registered axes on their standard properties and reserve font-variation-settings for custom axes.

Step 4: Fix the Inheritance Trap with Custom Properties

font-variation-settings is a single property that holds every axis at once. Setting it on an element replaces the whole list, so it does not merge with the parent's values:

/* The problem */
.card {
  font-variation-settings: "GRAD" 50, "XTRA" 480;
}

.card .highlight {
  font-variation-settings: "GRAD" 150; /* XTRA silently resets to default */
}

The .highlight element loses its XTRA value. On a large codebase, this causes text that looks subtly wrong in only some components.

The fix is to store each axis in its own CSS custom property and build font-variation-settings from all of them in one place:

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

body,
body * {
  font-variation-settings: "GRAD" var(--grad), "XTRA" var(--xtra);
}

.card {
  --xtra: 480;
}

.card .highlight {
  --grad: 150;
}

Now components change one axis by changing one variable, and everything else is preserved because custom properties inherit individually. The declaration has to sit on every element (hence body *), because an inherited font-variation-settings value is already resolved; only an element that declares the property itself picks up a changed variable. If the font family changes in part of the page, give that section its own font-variation-settings list with the axes that family supports.

A cleaner variant avoids the universal selector by declaring the settings on every element that sets an axis variable:

.vf {
  font-variation-settings: "GRAD" var(--grad, 0), "XTRA" var(--xtra, 468);
}

Add class="vf" to components that use custom axes, or include .vf in your base text styles.

Step 5: Load the Font

From the Google Fonts API

Request axis ranges with the .. syntax. Axes are listed alphabetically, lowercase tags before uppercase ones:

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght,GRAD@8..144,100..1000,-200..150&display=swap"
/>

This loads Roboto Flex with optical size, weight, and grade. Request only the axes you actually use; each extra axis adds bytes.

For italics in the same request, add the ital axis:

https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap

Self-hosted

For better privacy and performance, download the WOFF2 files and declare them yourself as in Step 1, then preload the critical one:

<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin />

If you only need part of a font's range, you can cut a smaller file with fontTools before deploying:

fonttools varLib.instancer RobotoFlex.ttf wght=300:800 opsz=drop GRAD=drop -o RobotoFlex-subset.ttf
pyftsubset RobotoFlex-subset.ttf --unicodes="U+0000-00FF,U+2000-206F" \
  --flavor=woff2 --layout-features="*" --output-file=roboto-flex-var.woff2

opsz=drop pins the axis at its default value and removes it from the file.

Step 6: Provide Fallbacks

Variable font support is effectively universal in 2026, but your font can still fail to load. Because you used standard properties for weight and width, fallback fonts already render at sensible weights. You can also feature-detect when you want different styling:

h1 {
  font-weight: 700;
}

@supports (font-variation-settings: normal) {
  h1 {
    font-weight: 780;
    font-variation-settings: "GRAD" 50;
  }
}

Also consider disabling synthetic styles if your family does not include an italic, so the browser does not fake one:

body {
  font-synthesis: none;
}

Using Variable Fonts in Tailwind CSS v4

Tailwind v4 is configured in CSS. Register the family in @theme, and add custom weight tokens if you want named steps between the defaults:

@import "tailwindcss";

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

@theme {
  --font-sans: "Inter", system-ui, sans-serif;
  --font-weight-book: 450;
  --font-weight-heavy: 780;
}

That gives you font-book and font-heavy utilities alongside the defaults. For one-off values, use arbitrary values:

<h1 class="font-[720] font-stretch-[85%]">Quarterly results</h1>
<p class="[font-variation-settings:'GRAD'_100]">Graded text</p>

Using Variable Fonts in WordPress theme.json

In a block theme, declare the font face with a weight range in theme.json version 3:

{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 3,
  "settings": {
    "typography": {
      "fontFamilies": [
        {
          "name": "Inter",
          "slug": "inter",
          "fontFamily": "Inter, system-ui, sans-serif",
          "fontFace": [
            {
              "fontFamily": "Inter",
              "fontWeight": "100 900",
              "fontStyle": "normal",
              "src": ["file:./assets/fonts/inter-var.woff2"]
            }
          ]
        }
      ]
    }
  }
}

WordPress generates the @font-face rule for you, and the editor's weight picker can use any value in the range. The full setup is in how to change fonts in a WordPress block theme using theme.json.

Debugging Variable Fonts

  • Firefox Font Editor. In the Inspector, open the Fonts tab. For variable fonts it shows a slider for every axis, and changes apply live to the selected element.
  • Chrome Rendered Fonts. In the Computed tab, Rendered Fonts confirms that the variable file, not a fallback, is in use.
  • Synthesized bold check. If bold text looks blurry or smeared, your @font-face is missing a weight range.
  • Axis value check. If a custom axis does nothing, confirm the tag's capitalization and that the file you loaded includes that axis; Google Fonts and subsetting tools often drop axes you did not request.

Variable Fonts in CSS FAQ

Almost always because the font-face rule declares a single weight instead of a range. Change it to a range such as 100 900 so the browser knows the file covers every weight. Without it, the browser picks the default weight or synthesizes bold.

Use font-weight. It inherits correctly, works with strong and b elements, and applies to fallback fonts. Reserve font-variation-settings for custom uppercase axes like GRAD or CASL that have no dedicated CSS property.

font-variation-settings replaces the entire list of axes whenever it is set. If a child sets one axis, every other axis returns to its default. Store each axis in its own custom property and build the property from all of them in one place.

Not in current browsers, which accept a variable font with a plain woff2 format hint. Listing format woff2 with tech variations, plus the older woff2-variations value as a second source, is the most defensive option if you want to cover every case.

Yes. Any number within the range declared in your font-face rule and supported by the font is valid. That is one of the main advantages of variable fonts over static families.

With the Google Fonts API, list only the axes you need in the URL. For self-hosted files, use the fontTools varLib.instancer command to limit ranges or drop axes, then subset and compress the result to WOFF2.

Conclusion

Using variable fonts in CSS comes down to a few rules. Declare ranges in @font-face so the browser knows one file covers many styles. Control weight, width, slant, italic, and optical size with their standard properties. Use font-variation-settings only for custom axes, and drive it with one custom property per axis so components can change a single axis without wiping out the rest.

Once that foundation is in place, the rest is straightforward: request only the axes you need, preload the critical file, and plug the same family into Tailwind's @theme or WordPress's theme.json. From there you can tune weights to the exact value your design needs, adapt width to the viewport, and even animate font axes for interactive typography.

Here are some useful references for going deeper on variable fonts in CSS:

  1. MDN Web Docs: Variable fonts guide — registered axes, custom axes, and the CSS that controls them.
  2. MDN Web Docs: font-variation-settings — syntax, inheritance, and interaction with high-level properties.
  3. W3C: CSS Fonts Module Level 4 — the specification for font-face ranges, tech(variations), and font-optical-sizing.
  4. Google Fonts: CSS API update — the axis range syntax for requesting variable fonts.
  5. fontTools: varLib.instancer documentation — limiting axis ranges and pinning axes in variable font files.
Share :

Related Posts

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

You align an icon next to a button label and it looks a pixel or two too high, no matter how you adjust vertical-align. You set overflow: hidden

Continue Reading
Are Google Fonts GDPR-Compliant?

Are Google Fonts GDPR-Compliant?

In late 2022, thousands of small business owners in Germany and Austria opened letters demanding a few hundred euros in "damages" because their websi

Continue Reading
How to Audit Web Font Performance with Lighthouse?

How to Audit Web Font Performance with Lighthouse?

A client sends you a screenshot of their PageSpeed Insights report: performance score 61, LCP 3.9 seconds, and a vague list of warnings. They want to

Continue Reading