The Complete Guide to Variable Fonts
A variable font is one font file that holds a continuous design space instead of a fixed list of styles. You pick any point along its axes, such as weight from 1 to 1000 or width as a percentage of normal, and the browser interpolates the outlines. On the web you declare the range in @font-face, then use ordinary properties like font-weight: 560; font-variation-settings is the low-level tool for custom axes.
That definition hides most of the practical questions: which axes a font really has, what the numbers mean, why a weight change sometimes does nothing, and when a single variable file is actually heavier than the static files it replaces. The sections below answer those in the order a designer or front-end developer usually runs into them.
How a variable font stores its styles
A static family ships one file per style: Regular, Italic, Medium, Bold, and so on. A variable font ships one default set of outlines plus instructions for how each point moves as you travel along an axis. The OpenType specification calls the table that lists those axes fvar, and every axis has a minimum, a default and a maximum.
When CSS asks for weight 560, the renderer works out how far 560 sits between the font's defined positions and moves every glyph point by that fraction. Nothing is stored for 560 specifically. That is why a variable font can honor values a foundry never named, and also why its quality depends on how carefully the designer drew the extremes and any intermediate positions.
Two consequences matter in practice:
- Named instances are labels, not files. A variable font may advertise "SemiBold" at 600, but you are free to use 590 or 615. The label is a convenience for design apps.
- The default instance is what you get when nothing is set. For the weight axis the specification requires the "Regular" value to be 400, so a correctly built font renders its normal text weight when CSS asks for nothing special.
The five registered axes, with the numbers the spec actually uses
Microsoft hosts the OpenType design-variation axis registry, and it lists exactly five registered axes. The ranges and scales below come from the individual axis pages of OpenType 1.9.1 on Microsoft Learn.
| Axis | Tag | Valid range in the spec | What a value means | Required or recommended default | CSS that drives it |
|---|---|---|---|---|---|
| Weight | wght | 1 to 1000 | Compares directly with CSS font-weight and the OS/2 weight class | 400 is required for Regular | font-weight |
| Width | wdth | Greater than 0 | Percentage of the designer's normal width | 100 is required for Regular | font-stretch (renamed font-width in the Level 4 draft) |
| Slant | slnt | Greater than -90 and less than +90 | Angle in counter-clockwise degrees from upright, so a right-leaning oblique is negative | 0 is required | font-style: oblique <angle> (opposite sign convention) |
| Italic | ital | 0 to 1 | 0 is the non-italic design, 1 is fully italic | 0 for upright | font-style: italic |
| Optical size | opsz | Greater than 0 | Text size in typographic points (1/72 inch) | 10 to 16 recommended for text settings | font-optical-sizing |
A few things in this table surprise people:
- The spec range is not the font's range. "1 to 1000" is what the weight axis may legally hold. A given font might only cover 300 to 800. Always read the font's own minimum and maximum.
- Slant is negative for the usual lean. The specification measures counter-clockwise, matching the
italicAnglefield in the font'sposttable. A typical oblique leaning right has a value like -10, not +10. CSS uses the opposite convention: MDN'sfont-stylepage says positiveobliqueangles "are slanted to the end of the line", which is a right lean in left-to-right text, within a range of -90deg to 90deg and a default of 14deg. So a value read from a font'sslntaxis changes sign when you write it as a CSSobliqueangle. - Slant and italic are different axes. The registry says fonts may use one or the other "but should rarely use both." Italic usually swaps letter shapes (a single-storey a, for example). Slant only leans the upright shapes.
- Width is open-ended. The spec sets no upper bound. A family might run from 75 to 125, or a foundry might ship a much wider span; read the font's own limits.
Custom axes
Foundries can invent their own axes. The registry requires foundry-defined tags to start with an uppercase letter and use only uppercase letters or digits, so they never collide with registered lowercase tags. Specimen pages show tags such as GRAD or XHGT. What each one does is defined by that foundry for that family, so a GRAD setting that works in one font has no guaranteed meaning in another.
MDN's font-variation-settings page makes the same point from the CSS side: axis tags are case-sensitive, and while registered axes use lowercase and custom axes should use uppercase, "font designers aren't forced to follow this practice." Copy tags exactly as the font's documentation spells them.
Optical size: the axis people underuse
Optical size deserves a section of its own because it works without any code. The opsz specification describes the axis as varying the design "to suit different text sizes", with typical fonts adapting glyph proportions, stem weights and details for legibility when small and refinement when large. MDN's font-optical-sizing page puts it in visual terms: small sizes "are often rendered with thicker strokes and larger serifs", larger text "more delicately with more contrast".
The CSS property has two values, auto and none, and the initial value is auto. MDN states that optical sizing "is enabled by default for fonts that have an optical size variation axis" and lists the property as Baseline widely available since March 2020. The W3C CSS Fonts Level 4 draft says the browser should supply an opsz value "close to the used value for font-size".
Two details are easy to miss:
- Points versus CSS pixels. The OpenType scale is points. CSS sizes are usually in pixels. The opsz specification warns against assuming equivalences between units "that may apply on some platforms but not others". If a heading looks slightly too delicate or too sturdy, the cause can be this mapping, and the fix is an explicit
font-variation-settings: "opsz" <value>on that element. - Clamping, not resetting. If the rendered size falls outside the font's opsz range, the spec says selection "should be clamped to the appropriate minimum or maximum axis value, not reset to the default instance." A 200px hero set in a font whose opsz tops out at 144 simply uses 144.
Writing the CSS
Step 1: declare the ranges in @font-face
The CSS Fonts Level 4 draft lets the font-weight, font-width (or font-stretch) and font-style descriptors take a range instead of a single value. The range tells the browser which requests this one file can satisfy. The family below is a hypothetical example.
@font-face {
font-family: "Example Serif VF";
src: url("/type/example-serif-vf.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Example Serif VF";
src: url("/type/example-serif-italic-vf.woff2") format("woff2");
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
The second block covers families that ship italics as a separate variable file rather than through an ital axis. Some families do one, some the other, so check which arrangement your font uses before writing the declarations. If the family instead has a slnt axis, declare an oblique range on the upright face (for example font-style: oblique 0deg 12deg). Without that, MDN's font-style page explains that an oblique request falls back to a face classified as italic, and only then to a synthesized slant.
Format hints. Older tutorials write format("woff2-variations"). MDN lists that string as an "older non-normalized" value kept "for backward-compatibility reasons", equivalent to format(woff2) tech(variations), the normalized form the current spec describes. The examples here use plain format("woff2"), because a browser skips a source entry it cannot use, and a variable WOFF2 is still a WOFF2 file. If you adopt tech(), test it in every browser you support first. Our WOFF2 vs WOFF vs TTF guide covers why WOFF2 is the only file most sites need to serve at all.
Step 2: use the high-level properties first
body { font-family: "Example Serif VF", Georgia, serif; font-weight: 380; }
h1 { font-weight: 720; }
strong { font-weight: 640; }
.legal { font-size: 0.8125rem; } /* opsz follows automatically */
Using font-weight, font-stretch and font-style keeps the stylesheet readable, inherits normally, and degrades sensibly if the variable file fails and a static fallback loads. MDN's guidance is to use font-variation-settings "only when no basic properties exist to set those features".
Step 3: use font-variation-settings only for custom axes or deliberate overrides
.card-title {
font-weight: 650;
font-variation-settings: "GRAD" 50, "XHGT" 0.7;
}
Weight stays on the normal property and only the two foundry-defined axes go through the low-level one.
The precedence rule, stated correctly
This is where many tutorials, including an earlier version of this page, get the direction wrong. The W3C CSS Fonts Level 4 draft lists the order in which font features and variations are applied, with later steps winning. Variations from font-weight, font-width and font-style come in early (step 2 of 13). Variations from font-optical-sizing come later (step 9), and the font-variation-settings property comes near the end (step 12).
So for the same axis, font-variation-settings wins over font-weight regardless of the order of the two declarations. MDN says the same: values set through font-variation-settings "will always override" the basic properties "no matter where they appear in the cascade", adding that in some browsers this holds only when the @font-face rule declares a weight range.
What causes real bugs is a different behavior: font-variation-settings is one property, so a later declaration replaces the whole list. If a base rule sets "GRAD" 50, "XHGT" 0.7 and a hover rule sets only "GRAD" 0, the hover state silently loses XHGT. Either repeat every custom axis in each declaration, or drive each axis from a custom property:
.card-title {
--grad: 50;
--xhgt: 0.7;
font-variation-settings: "GRAD" var(--grad), "XHGT" var(--xhgt);
}
.card-title:hover { --grad: 0; }
What happens with bad values
The CSS Fonts draft covers both cases. A tag the font does not support is ignored, and the other axes in the same declaration still apply. A value beyond the font's range is "clamped to the closest value supported by the font." Neither case produces an error, which is why a typo in an axis tag fails silently.
Worked example: does one variable file pay off?
The file-size comparison depends entirely on your font, so the honest way to decide is to measure your own files. Here is the arithmetic with stated inputs. The byte figures are an illustration, not measurements of a particular family.
Inputs (illustrative). A site uses one sans family in Regular, Medium, SemiBold and Bold, upright only, Latin subset. Each static WOFF2 file is 24 KB. The variable WOFF2 covering weight 100 to 900, same subset, is 68 KB.
| Scenario | Files requested | Bytes | Requests |
|---|---|---|---|
| Static, 4 weights | 4 | 4 x 24 = 96 KB | 4 |
| Variable, weight axis only | 1 | 68 KB | 1 |
| Static, only Regular and Bold used | 2 | 48 KB | 2 |
| Variable, still 1 file | 1 | 68 KB | 1 |
With four weights in use, the variable file saves 28 KB and three requests. If the design is later trimmed to two weights, the static pair wins by 20 KB. The break-even point in this illustration sits between two and three static files (68 divided by 24 is about 2.8).
To repeat this with your own fonts:
- Subset both versions to the same character set first, otherwise you are comparing language coverage, not formats.
- Compress both to WOFF2.
- Count the styles that load on a typical page, not the styles that exist in the design file. A weight used only on the pricing page should not count toward the home page.
- Divide the variable size by the average static size. If the answer is lower than the number of styles you load, the variable file is lighter.
Subsetting often moves the numbers more than the variable choice does. Google's CSS API documentation says a text= request naming only the characters you need "can reduce the size of the font file by up to 90%" in some cases. For self-hosted files, the subsetting workflow in our format guide applies to variable fonts too, with one caution: after subsetting, open the output in a font inspector and confirm it still lists its axes, then test it in a browser before shipping it.
Requesting variable fonts from Google Fonts
If you load from the Google Fonts CSS API instead of self-hosting, the axis range goes in the URL. The CSS2 API documentation says to "join the 2 values with ..", for example family=Crimson+Pro:wght@200..900. Multiple axes are listed alphabetically, and value tuples are sorted numerically:
https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,200..900;1,200..900&display=swap
The display parameter passes a font-display value; Google's docs say a value other than the default auto "is usually appropriate". Licensing duties per family and the privacy argument for self-hosting are covered in our Google Fonts commercial use and self-hosting guide.
Animating axes without hurting the page
Because axes are continuous, CSS transitions work on them. MDN lists the animation type of font-variation-settings as "a transform", so it interpolates between values.
.tab {
font-weight: 420;
transition: font-weight 180ms ease-out;
}
.tab[aria-selected="true"] { font-weight: 640; }
@media (prefers-reduced-motion: reduce) {
.tab { transition: none; }
}
Three cautions before you animate:
- Weight usually changes width. The wght axis definition notes that secondary details "such as glyph width" may vary along with stroke thickness. A bolder word takes more room, so animating weight inside running text reflows the line every frame. Animate elements whose box size is fixed (a tab with a set width, a button with spare padding), or use a font whose documentation describes a width-neutral custom axis.
- Keep it to a few elements. Text re-layout on every frame costs main-thread time. Our web animation performance guide explains why layout-triggering animations are the expensive kind and how to check them in DevTools.
- Respect reduced motion. Wrap decorative axis animation in a
prefers-reduced-motioncheck, as above.
Fallbacks, synthesis and debugging
Faux bold and faux italic
If CSS asks for a weight or style the font cannot supply, the browser may fake it. MDN's font-synthesis page says browsers may "synthesize the bold, italic, small-caps, and/or subscript and superscript typefaces when they are missing", and the initial value allows all four. With a variable font, synthesis usually points to one of two mistakes: the @font-face range is narrower than the weights you request, or you asked for italic from a file with no italic axis and no italic companion file.
A useful development habit: set font-synthesis: none while building. Missing styles then render plainly wrong instead of quietly faked, which makes them easy to spot. When the problem turns out to be a file that never loads at all, our web fonts not loading troubleshooting guide works through it symptom by symptom.
A debugging order that finds most problems
- Confirm the axes exist. Open the font in a font inspector (several free web-based and desktop tools list a file's
fvaraxes and ranges) or read the foundry's specimen. A file sold as "variable" may expose only weight. - Check the @font-face range. A declaration of
font-weight: 400on a variable file tells the browser it only covers 400. Requests for 700 then trigger synthesis or a different face. - Look for a later
font-variation-settingsreplacing the list. Search the stylesheet for every declaration on the element and its states. - Check tag spelling and case.
"WGHT"is not"wght". - Read the computed value. In DevTools, a computed weight like 637 is normal for variable fonts; it means interpolation is working.
Fallback fonts and layout shift
Variable or not, the swap from fallback to web font can move text. The size-adjust descriptor, which MDN marks Baseline widely available since September 2023, scales a fallback face's metrics so it occupies roughly the same space as the web font:
@font-face {
font-family: "Example Serif fallback";
src: local("Georgia");
size-adjust: 104%;
}
The 104% is a placeholder. Tune it by overlaying the two fonts at the same size until line breaks match.
Decision framework: variable, static, or both?
| Your situation | Choose | Reason |
|---|---|---|
| Three or more weights of one family load on common pages | Variable | One request, and usually fewer bytes once you pass the break-even point from the worked example |
| One or two weights, no plans to add more | Static | The variable file carries outline deltas you never use |
| Design needs in-between weights (for example 450 for text on dark backgrounds) | Variable | Static families rarely ship those values |
| Display headline needs an optical cut beyond the variable font's opsz range | Static display cut | Clamping means the variable font stops at its maximum opsz |
| Browsers in your own analytics cannot render variations | Variable plus a static fallback in the src list, or static only | Keep the support matrix honest instead of guessing |
| Font has custom axes you plan to animate | Variable | Only possible with variations |
For licensing, nothing changes because a font is variable: the same license file governs it. With a commercial foundry, confirm that the web license covers the variable file specifically, since a foundry may sell it as a separate product. The checks are in our commercial font licensing guide.
Mistakes that show up after the switch
- Keeping the old static
@font-faceblocks after switching. Pages then download both. Delete the static rules or keep them only as a deliberate fallback. - Declaring a single weight on a variable file. The descriptor must be a range or the browser will not use the file for other weights.
- Using
font-variation-settingsfor weight. It works, but it overrides inheritedfont-weightvalues and breaks<strong>and<b>styling inside that element. Usefont-weight. - Mixing up the two slant conventions. A right lean is negative on the
slntaxis but positive as a CSSobliqueangle. Copying a number from a font specimen into CSS without checking which scale it uses leans the text the wrong way. - Assuming every variable font has opsz. Without an
opszaxis,font-optical-sizinghas nothing to adjust. - Comparing file sizes before subsetting. Compare like with like or the decision is noise.
Variable font questions we get asked
What does the number in font-weight: 550 actually change?
It sets the position on the font's wght axis. The renderer interpolates every glyph between the nearest defined positions, so 550 is a distinct design point, not a rounding to 500 or 600. If the font's range stops at 500, 550 is clamped to 500.
Can I use variable fonts in emails or PDFs?
That depends on the receiving software rather than on any web standard. Email support for web fonts varies by client, so test in the clients your audience uses and make sure the fallback stack looks acceptable on its own. For PDFs, check how your design tool exports variable fonts by opening the exported file and inspecting its embedded fonts. Licensing is usually the simpler part: for OFL fonts, the SIL OFL FAQ allows embedding in documents "either in full or a subset."
Why does my italic look like slanted upright letters?
Either the font has only a slnt axis (an oblique by design), or the browser is synthesizing italic because no italic face was found. Check the axis list first. If there is an ital axis or an italic file and you still see a mechanical slant, the italic @font-face rule is missing or its font-style descriptor is wrong.
Do I need font-optical-sizing in my CSS at all?
Usually not. Its initial value is auto, so fonts with an opsz axis already adapt. Set it to none only when you want one fixed optical design across sizes, for example to match a static logo lockup exactly.
Are variable fonts accessible?
The format itself is neutral. The accessibility questions are the usual ones: enough contrast, readable sizes, and layouts that survive the WCAG 2.2 text spacing test (line height 1.5 times the font size, letter spacing 0.12 times, word spacing 0.16 times). Very light weights from a wide range can look faint at small sizes even when the color passes contrast, so test thin weights at real sizes.
Sources used for this guide
Each source was read for this update and checked October 22, 2026:
- OpenType Design-Variation Axis Tag Registry, OpenType 1.9.1, Microsoft Learn: https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg (checked October 22, 2026)
- wght axis definition: https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_wght (checked October 22, 2026)
- wdth axis definition: https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_wdth (checked October 22, 2026)
- slnt axis definition: https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_slnt (checked October 22, 2026)
- opsz axis definition: https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_opsz (checked October 22, 2026)
- W3C CSS Fonts Module Level 4, Working Draft of 16 October 2026: https://www.w3.org/TR/css-fonts-4/ (checked October 22, 2026)
- MDN, font-variation-settings: https://developer.mozilla.org/en-US/docs/Web/CSS/font-variation-settings (checked October 22, 2026)
- MDN, font-optical-sizing: https://developer.mozilla.org/en-US/docs/Web/CSS/font-optical-sizing (checked October 22, 2026)
- MDN, @font-face src (format and tech keywords): https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src (checked October 22, 2026)
- MDN, font-style (oblique angles and italic fallback): https://developer.mozilla.org/en-US/docs/Web/CSS/font-style (checked October 22, 2026)
- MDN, font-synthesis: https://developer.mozilla.org/en-US/docs/Web/CSS/font-synthesis (checked October 22, 2026)
- MDN, size-adjust: https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/size-adjust (checked October 22, 2026)
- Google Fonts CSS API v2 documentation: https://developers.google.com/fonts/docs/css2 (checked October 22, 2026)
- SIL Open Font License FAQ: https://openfontlicense.org/ofl-faq/ (checked October 22, 2026)
- W3C, Understanding WCAG 2.2 Success Criterion 1.4.12 Text Spacing: https://www.w3.org/WAI/WCAG22/Understanding/text-spacing.html (checked October 22, 2026)
Where to go next
Variable fonts solve the file-count problem, but they do not choose the type for you. Our guide to choosing fonts for web design works through categories, pairing, readability tests and a selection workflow, which is the right next read if you are still deciding which family deserves a variable file in the first place.