Typography

CSS @font-face Guide: WOFF2, Variable Fonts and Fallbacks

By Aigars Silkalns Updated 6 min read

CSS @font-face Guide: WOFF2, Variable Fonts and Fallbacks

Writing @font-face is no longer the hard part. Every current browser supports it. What goes wrong now is loading: a slow font download makes text invisible or makes the page jump when the font arrives. A good setup ships one WOFF2 file, picks a deliberate font-display value, preloads the critical file and uses a fallback font tuned to match the real one.

This guide walks through that setup. Years ago, developers used Modernizr to detect missing @font-face support and swap in a hand-styled .no-fontface fallback. That problem is gone, but the idea behind it, a fallback that matches the custom font, matters more than ever.

Ship WOFF2 and nothing else

WOFF2 is supported by all modern browsers, so one src line is enough. You no longer need EOT, TTF or SVG fonts, and WOFF1 is only worth keeping if you must support very old browsers.

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

The format("woff2") hint lets a browser skip files it cannot use without downloading them. If you want only Latin text, subset the font and serve a unicode-range per script, so visitors download just the glyphs the page needs. Each subset is its own @font-face block, and the browser fetches a file only when the page contains a character in its range:

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-latin.woff2") format("woff2");
  font-weight: 100 900;
  unicode-range: U+0000-00FF, U+0131, U+2013-2014, U+2018-201D, U+20AC;
}
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-latin-ext.woff2") format("woff2");
  font-weight: 100 900;
  unicode-range: U+0100-02BA, U+1E00-1EFF, U+2020, U+20A0-20AB;
}

If a fallback has a different x-height than your web font, font-size-adjust: from-font (or a fixed number such as 0.5) keeps lowercase letters at a similar visual size across the swap.

Use a variable font for multiple weights

A variable font packs a range of weights (and sometimes width or slant) into one file. Instead of four requests for Regular, Medium, Semibold and Bold, you declare a weight range and use any value in it.

The font-weight: 100 900 line above is the key. With it, font-weight: 450 or 650 works in your CSS, and the browser interpolates. Other axes are reached with font-variation-settings, but prefer the high-level properties (font-weight, font-stretch, font-style) whenever the font exposes them.

A variable file is larger than a single static weight, so it pays off when you use roughly three or more weights or want fine control. For a single regular weight, a static subset can be smaller.

Choose font-display on purpose

The font-display descriptor controls what the browser shows while the font downloads. It is divided into a block period (text is invisible) and a swap period (a fallback shows and is replaced when the font arrives).

Value Block period Swap period Use it for
swap Extremely small Infinite Body text and headings where the brand font matters
fallback Extremely small Short A compromise: swap only if the font arrives quickly
optional Extremely small None Non-critical fonts; the browser may skip them on slow connections
block Short Infinite Icon fonts, where a fallback glyph would be meaningless

The default, auto, leaves the decision to the browser, which usually behaves like block. Set the value explicitly. With swap, the layout shift risk is real, which brings us to the fallback.

Preload the one font that matters

Browsers discover a font only after they have parsed the CSS and found text that uses it. Preloading moves that discovery earlier. Do it for the one or two files used above the fold, not for every weight.

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

The crossorigin attribute is required even for same-origin fonts, because fonts are fetched in anonymous CORS mode. Without it the browser downloads the file twice.

Self-host instead of linking a font CDN

Serving fonts from your own domain removes a DNS lookup and a TLS handshake to a third-party host, gives you control over caching headers and subsetting, and avoids sending visitors’ IP addresses to a third party, which some privacy rules care about. Download the font files from the foundry or from an open-source distribution (the Fontsource packages on npm bundle many Google Fonts as WOFF2), check the font licence, and put them in your own static folder. Serve them with a long Cache-Control lifetime, since a font file’s name changes when its content does.

Build a fallback that matches the web font

A fallback font that differs in width or height makes the layout jump when the web font replaces it. Picking a similar-looking system font helps, but each font has its own width, ascent and descent, so the match is never exact.

Modern CSS fixes this with a second @font-face rule that wraps a local font and adjusts its metrics to match the web font:

@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  font-weight: 400;
  size-adjust: 107.1%;
  ascent-override: 90.5%;
  descent-override: 22.5%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "Inter Fallback";
  src: local("Arial Bold"), local("Arial-BoldMT");
  font-weight: 700;
  size-adjust: 107.1%;
  ascent-override: 90.5%;
  descent-override: 22.5%;
  line-gap-override: 0%;
}

body {
  font-family: "Inter", "Inter Fallback", system-ui, sans-serif;
}

Here is where the numbers come from:

  • size-adjust scales the fallback glyphs. I measured the same sample sentence at 100px in Arial and in Inter; Inter was about 7.1% wider, so Arial is scaled to 107.1%.
  • ascent-override and descent-override set the line box. Take the web font’s ascent and descent from its hhea table, divide by units per em (Inter: 1984/2048 and 494/2048, that is 96.9% and 24.1%), then divide by the size-adjust factor, because the overrides are applied before scaling. That gives about 90.5% and 22.5%. These figures are for Inter 4.x (2048 units per em) paired with Arial; other Inter builds have different hhea metrics, for example the Inter 3 files use 2816 units per em, so measure the file you actually serve.
  • line-gap-override is 0% because Inter’s line gap is zero.

You can read those metrics with a tool such as the open-source fontTools Python library, or let a build tool such as Fontaine or the Next.js font loader compute them for you.

The result

The demo below sets the same heading and paragraph three ways: a plain Arial fallback, the adjusted fallback above, and Inter after it loads. Each card reports where its lines break. Plain Arial breaks the headline after “without” and the paragraph after “font,” and “the”, so the text reflows when Inter arrives. The adjusted fallback breaks in the same places as Inter.

Three cards with the same headline: plain Arial wraps differently, while the metric-adjusted fallback matches the line breaks of the loaded Inter font
Left: plain Arial. Middle: adjusted fallback. Right: the loaded web font. Measured in Chromium: Arial’s headline breaks one word later than Inter’s, and its paragraph breaks at different words on every line; the adjusted fallback matches Inter on all of them.

Browser support differs between descriptors. Per MDN’s compatibility data (checked 30 October 2026), size-adjust works in Chrome and Edge 92, Firefox 92 and Safari 17. The ascent-override, descent-override and line-gap-override descriptors work in Chrome and Edge 87 and Firefox 89, but Safari lists them only in Technology Preview, so MDN does not call them Baseline. Treat the block as progressive enhancement: where an override is ignored, the fallback is Arial scaled by size-adjust and your page still works. Re-check the compatibility table before relying on it.

What happened to Modernizr and .no-fontface

You do not need feature detection for @font-face any more. If you maintain old code with a .no-fontface rule, delete it together with the Modernizr script. For the rare case where you do want to branch on a CSS feature, @supports does it natively with no JavaScript.

You can also react to the actual loading state with the CSS Font Loading API, for example by waiting on document.fonts.ready before running measurements, as the demo for this article does.

Checklist

  1. Serve WOFF2 only, subset to the languages you need.
  2. Use a variable font if you need several weights.
  3. Set font-display explicitly: swap for brand text, optional for nice-to-have fonts, block for icon fonts.
  4. Preload the one or two fonts used above the fold, with crossorigin.
  5. Self-host with long cache lifetimes.
  6. Add a metric-adjusted fallback face and put it second in your font-family stack.

For more on the topic, see @font-face Solutions & Suggestions, learn how to add icon fonts with CSS (where block is the right choice), or browse 30 free fonts for creative projects to find something to load. If you want more techniques like this one, 20 pro CSS tips is a good next stop.

54 comments from the archive
  1. this article is very timely for me, just implementing @font-face on a project now ;)

  2. Nice Guide, but the demo’s are not working getting a 404 keep up the good work

  3. Handy for me! I was actually searching for some information on webfonts and @font-face & this guide just perfect for it :) TY

  4. Yeah, a very helpful text, thanks! I’ve been thinking about using @font-face and this article gave me clues (and solutions) about possible problems.

  5. This post explains very nicely about custom fonts, picking the matching fallback web safe fonts, and how to create a perfect fallback font style with Modernizer….REALLY A GOOD WORK!keep it up! and keep updating us!

  6. Nice job. Very well put together article and full of useful info.

    It’s starting to get crazy with all of the scripts we need to include these days. jQuery, Modernizr, etc. I can’t wait until we can get some more uniform browser support. But, it seems like this type of request hasn’t really changed since the dawn of the browser wars. [sigh]

  7. I must say, thank you for the article, I’m a big fan of @font-face and this is a big help for me, for my future projects :D thanks

  8. What’s the benefit to using Modernizr for matching font styles rather than just adding them in the CSS for the class you are targeting? In your example:

    h1 {
    font-family: “Times New Roman”, Palatino, Georgia, serif;
    }

    I am not really getting why Modernizr does this better, especially when it’s adding more load time to your page and does (as far as I can tell from this tutorial) exactly the same thing as backup fonts in CSS.

    Can anyone clarify if I’m missing something?

    1. If you observe the examples, using the no-fontface class, you are not only changing the font, but also changing other characteristics such as size, weight, etc. Example shows Wire One 48pt upper case, with a no-fontface fallback of Arial 26 normal. Simply adding Arial to the font stack would result in Arial 48 upper case. So, basically, you can style your no-fontface font to fit the space provided in your layout. It’s a complete alternate style fallback that results in a similar-enough experience for all users.

      1. I see now. Sorry I’m a bit new to advanced CSS/etc and didn’t pick up on that. Thanks for the clarification!

  9. Thanks for pointing out the importance of fallback fonts, Nick. I’m with ya!

    In addition to @font-face, this idea can also be applied to various font-embedding services. In my experience with Typekit, the default font stack I copy from the kit looks something like this:
    font: 'tk-sans-1', 'tk-sans-2', serif
    Obviously not ready for production. Typekit also provides some control that looks like it does some of what Modernizr is doing above.

  10. Nice article.

    But what happens if the browser supports @font-face but the font is not found (‘Wire One’ in your example)? Will we still see the fallback font out of boundary?

    Back to the example, if the browser supports @font-face but the font ‘Wire One’ is not found we will see Arial 48px instead of Arial 30px, correct?

    1. The browser downloads the font from the server and loads it. If the font not found on the server due to error, or the browser can’t load the font then you are right, the original 48px arial will be displayed.

      There is another css3 property – I hope will be implemented in browsers – is the font-size-adjust, which basicly sets the font size by the x-height, not the capital letter height. This is also useful for font fallbacks. Unfortunately it is only implemented by Mozilla right now (who proposed the property and in Firefox since 1.0).

  11. Nick,
    Thanks much for clarifying some things. I have been “playing” around with @font face a bit and have run into snags.
    Always a pleasure reading your posts.
    Dale

  12. Very nice of course, but I would suggest a slight tweak that would handle rare, edge cases: what if JavaScript isn’t enabled on the browser AND the browser is not compatible with @font-face? Solution is quite simple: instead of using the no-fontface class generated by Modernizr, it would be best to first write the fallback CSS rule for fonts, that will be processed by every browser, no matter if there are JavaScript or not, and then write a CSS rule applying only to the “fontface” class generated by Modernizr. Since Modernizr generates both possibilities (“with or without the feature”), that allows the feature to degrade even more gracefully for non-Javascript enabled browsers. Only downside: if a browser has JavaScript disabled BUT @font-face… it will only display the fallback layout. Then we have to decide what is worst: for someone that has @font-face enabled but no JavaScript to see a perfectly suitable, though not-as-cool-as-it-could-be fallback layout, or for users without JavaScript AND without @font-face to see a bizarre layout, due to the rules based on a no-fontface class that won’t apply because there is no JavaScript… Hard choice, all depends on the targeted users, but that “inverse” option is pretty neat too, I think!

    1. I had the same remark…

      I guess it’s ok to give people who turned JavaScript off a less visual experience ans just use a web safe font for them.

  13. Hello, WebdesignerWall

    May you cover a tutorial on modernizer and how to use this library effectively?

    thank you!

  14. Nice article on web fonts!

    There’s one thing I miss. In my opinion the biggest problem with web fonts are not the browsers that don’t support @font-face, but those that do support it but don’t render most of web fonts correctly. I’m looking at IE on Windows XP. Even with clear type activated, the masses of web fonts look bad. I don’t see any other solution than UA sniffing here, but maybe there’s something else?

  15. Great post. I’ve heard of modernizer but never really looked into it. In my mind it was the same as PIE for ie CSS3 support. Thanks for the clarification and clear presentation of graceful degradation.

  16. I learn a new thing, it save my time to edit those styling text. Previously I am so painful to create such text in images format. The best is, you covered web safe fonts part for unsupported web browser.

  17. Hey Nick, another wonderful tutorial! Thanks!
    I’m working with @font-face and I’ve found that your tutorial is so useful.
    Yesterday I tried to find the best way to specify the fallback fonts and your tutorial has simplify this! Thank you

  18. For a post highlighting @font-face, it has an extreme dearth of @font-face content. None, to be precise. And especially nothing particularly CSS3. You mention fallbacks, which any designer worth their weight in artistic poseurishness knows. Then you shoehorn the modernizr JS lib which ‘automagically’ does stuff for you as far ask checking for capabilities. Which would be a perfect time to talk about the actual @font-family issue. The differences between using .EOT .OTF/.TTF .SVG fonts, their requisite support in major browsers, maybe the previous commentator’s recognition of IE’s decidedly lackluster support for any sort of decent hinting/smoothing for webfonts and how that basically breaks the spirit of webfonts….. I don’t know. Couldn’t help feeling letdown by this article, or at least it’s misleading title.

  19. Nice article. I know some will not like this but I’m not sure that @ffont-face is such a good idea. Could that lead us to a lot of pages which are illegible? We already have too many of those and giving designers a broader array of fonts may not be in the best interest of users. This we shall only see with time. I hope I am wrong! Will tweet this article.

  20. Thanks for the great tip! And thanks to Modernizer’s author/s! So i guess we dont need to write fallback fonts to WireOne since theres a specific fallback script?

  21. Ah I always make that mistake of not including a web safe fallback font, or any fallback font at all!

  22. Interesting tip. I still feel @ font-face would still take sometime for the developers to start using it, as it has its own fall outs.

  23. If the script is looking for browsers that don’t support @font-face/CSS3, do we really need to list fall back fonts for those browsers if we’ve set up .no-fontface rules for those browsers? I’d love to hear the reasoning behind that need. (I’ve always forgotten to set them up, but I’d like to think that styling with the modernizr script in mind makes that unnecessary.)

    1. There’s always the few out there who have javascript turned off AND running older browsers that don’t support @font-face.

      The .no-fontface is only used by Modernizr, so if no javascript is enabled in the browser, the .no-fontface styles will not be used either.

      It’s good practice not to rely 100% on javascript being turned on. However, if there’s no available fallback fonts defined, the browser will still display it (in it’s default font-face). It just won’t be pretty.

  24. Nice tutorial! But since there could be someone with javascript disabled, maybe it’s simpler to use the google fonts API. I would like to have some opinions :D

    1. I agree. I think this is a lot cleaner than a javascript solution, and Google has really built a nice collection of fonts.

  25. Love the idea of using modernizer to protect the layout when fallback fonts need to be used. I have also noticed that custom fonts don’t display correctly on mobile devices (even when using Droid font from Google API on an Android phone). Any solution for that?

  26. A unilateral mistake is where only one party to a contract is mistaken as to the terms or subject-matter contained in a contract. This kind of mistake is more common!

  27. I’d have to check with you here. Which is not something I usually do! I enjoy reading a post that will make people think. Also, thanks for allowing me to comment! thank you!

  28. You bring up some good points out about styling breaking if correct fonts aren’t used.

  29. Thanks, this was really useful checklist of some of the common font problems, I’ll definitely have a go with Modernizr, it sounds pretty useful

  30. wanted to use this in an older project, problem was that I couldn’t check if it worked! Couldn’t find a browser that didn’t support fontface (older ie versions worked perfect, ipad didn’t had any troubles with fontface, even with the free atomic browser..). if someone could tell me a browser that doesn’t support this pls tell me :)

  31. Great use of modernizr for this., but wouldn’t the last css-example still give layout issues, i.e: a browser supporting @font-face but lacking font ‘Wire One’ would fallback to arial font-size:48px NOT arial font-size: 30px.

  32. maybe… /* no-faceface fallback */ is should be /* no-fontface fallback */ ?
    nice tutorial, thank you.

  33. I love your blog and i love your ideas.i was having trouble with font selection and spending too much time to select the right one. Now i think this article may help me. Thanks a lot. I will surely use these tips in my blog Blogger Tricks.

  34. Just used this tutorial to add the bank gothic font to my site. Useful post, thanks a lot

  35. In your demo example the Google font “wire one” is used. Is it possible to use a font like for ex. Helvetica. You don’t have to include the Modernizr js when using the Google fonts I think?

  36. great examples i would not of thought of testing a fallback font to make sure it stays within boundaries etc. good suggestion and post.

  37. What is point of having a fallback font for your fontface style if it’s going to just fall back to your non-fontface solution?

  38. Such a great tutorial!
    Is there a website or a way that we can test to see if the script works?
    If I remove the custom font from my remote server whilst using the modernizr fallback as explained above, it doesn’t seem to pick up the modernizr fallback font. Am I testing it the wrong way?

    1. Dale,

      If I understand how Modernizr works correctly, it’s testing your browser to see if the browser supports @font-face. Whether or not your page actually uses @font-face isn’t tested. The fallback class is only activated if you are viewing the page with a browser that doesn’t support @font-face.

  39. I think Google Fonts you shouldn’t have any problems with fonts not rendering, and you don’t have to worry about backup fonts. Since fonts load via Google. I never had a problem with it.

  40. If I remove the custom font from my remote server whilst using the modernizr fallback as explained above, it doesn’t seem to pick up the modernizr fallback font. Am I testing it the wrong way?

  41. It does not work with Firefox when web fonts are disabled (about:config > gfx.downloadable_fonts.enabled > false) and all the content is displayed with fallback fonts (e.g. Arial). So how can I test it?

  42. I was thinking of finally going wild on my site redesign and finally breaking away from websafe fonts but wasn’t sure if it was a good idea – think i’ll give it a try with this fallback! Thanks!

Comments are closed on this article.