@font-face

Darshan Siroya on

Get affordable and hassle-free WordPress hosting plans with Cloudways — start your free trial today.

The @font-face at-rule lets us define custom fonts beyond the user’s pre-installed fonts, both from local and remote font services. In other words, we can bring our own custom font files to the party and CSS will download and use them.

@font-face {
  font-family: "CustomFont"; /* Reference name for the font */
  src: url("CustomFont.woff2") format("woff2"),  
       url("CustomFont.woff") format("woff"); /* Fallback for older browsers */
  font-weight: normal;
}

The @font-face at-rule is defined in the CSS Fonts Module Level 4 and CSS Fonts Module Level 5

Syntax

@font-face {
  <declaration-list>
}

Basic descriptors

Think of @font-face as a way to tell the browser exactly how to fetch and display font files. It comes with a bunch of handy descriptors that for fine-tuning things like where the font file is located, how bold it should be, and even which characters to load. However, in most situations, we will use the following descriptors:

  • font-family: The name we’ll use to apply the font in CSS
  • src: The location of the font file. It’s better to use multiple formats, so your font works smoothly on different browsers and devices.
  • font-weight: Sets the font’s thickness
  • font-style: Define the font’s style, i.e., normal or italic.

These are descriptors, not properties! While they bear the same name as certain font properties, they are actually descriptors that have a slightly different meaning inside the @font-face rule.

Basic usage

The @font-face rule allows us to load font files and use them in CSS, just like the fonts that are pre-installed on the user’s device. To define a “custom” font, we need to specify its name using the font-family descriptor and its source file using the src descriptor:

@font-face {
  font-family: "Open Sans";
  src: url("/fonts/OpenSans-Regular.woff2") format("woff2"), url("/fonts/OpenSans-Regular.woff") format("woff");
}

Once the font is registered, we can apply it anywhere in our styles.

body {
  font-family: "Open Sans", Arial, sans-serif;
  font-size: 16px;
  color: #333;
}

The browser will now load Open Sans as the primary font. If the font can’t load for some reason, it will fallback to Arial, and then the browser’s default sans-serif font.

We can also register multiple versions of the same font, such as bold or italic variants. In the case of a bold font, we use the font-weight descriptor:

@font-face {
  font-family: "Open Sans";
  font-weight: bold;
  src: url("/fonts/OpenSans-Bold.woff2") format("woff2");
}

Even though we are defining multiple fonts with the same font-family (Open Sans) in these examples, the browser will pick the normal or bold version automatically based on our CSS. So, for example, the following will check both the font-family and font-weight property to then pick the OpenSans-Bold.woff2 font file because we’re declaring that we want font-weight: bold.

h1 {
  font-family: "Open Sans", sans-serif;
  font-weight: bold;
}

We can do the same thing with the font-style descriptor, which defines the angle of the font. For example, we could set it to italic for a slight slant:

@font-face {
  font-family: "Open Sans";
  src: url("/fonts/OpenSans-Italic.woff2") format("woff2");
  font-style: italic;
}

This will load the italic font file (OpenSans-Italic.woff2), which will be used when an element’s font-family and font-style properties match, like this, when applied to an element:

h1 {
  font-family: "Open Sans", sans-serif;
  font-style: italic;
}

More descriptors!

The last section of descriptors covers the basics for @font-face, but there are still many other descriptors to get fonts to work exactly as we want. And even the last descriptors have a lot more nuance. Let’s dig deeper into those, as well as look at a few other available descriptors.

font-family

font-family: <string> | <custom-ident>+;

In the @font-face rule, the font-family descriptor is like a name tag for our custom font. It tells CSS what to call the font, so we can use it throughout our stylesheet. If the font name has spaces, wrap it in quotes; otherwise, leave it unquoted for simpler names.

Font name in quotes:

@font-face {
  font-family: "Open Sans"; /* Quoted string because the name contains a space */
  src: url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap"); /* Google Fonts URL */
}

And here it is without quotes since we’re giving it a single descriptor without spaces:

@font-face {
  font-family: RobotoSlab; /* Unquoted because it's a single-word identifier */
  src: url("https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@400;700&display=swap"); /* Google Fonts URL */
}

src

src: <url> [ format(<font-format>)]? [ tech(<font-tech>#)]? | local(<family-name>);

The src descriptor tells the browser where to grab the font file and how to load it. Adding multiple font sources helps make sure your font shows up correctly on all devices. But these days, you can likely get away with just specifying a .woff2 file, as those have great support among modern browsers.

  • <url>: Provide the font file’s address. This can be either a local path or a link to an online resource.
  • format(<font-format>): Here, we specify the type of font file we are serving. For example, we might use .woff2 for modern browsers and .woff as a reliable backup. This allows browsers to automatically select the most efficient format available to use.
  • tech(<font-tech>): The tech() function communicates a font’s special abilities to the browser, like, a font file that includes color SVG or is a variable font. Use tech(color-svg) for fonts with colorful SVG icons, tech(color-palette) for fonts with adjustable color schemes, or tech(variations) when working with variable fonts.
  • local(<family-name>): This lets you tell the browser, “Hey, if you’ve already got this font on your computer, use that one.” You specify the font’s name, e.g. local(Arial), and the browser checks its local stash before attempting to download the font file.
@font-face {
  font-family: "RobotoFlex";
  src: local("RobotoFlex"), url("RobotoFlex-Variable.woff2") format("woff2") tech(variations);
}

font-weight

font-weight: auto | <font-weight-absolute>{1,2};

The font-weight descriptor is all about how thick or bold the letters look. It’s an important part of making sure our font file matches a specific weight value.

  • auto: The browser automatically picks the most suitable font weight according to the context and styles applied around the text.
  • <font-weight-absolute>{1,2}: You can choose either one or two weight values. If one value is provided, for example, font-weight: 400, it applies a single weight. And if two values are provided, for example, font-weight: 400 700, it defines a range of acceptable weights.

There are a couple of key numbers that are good to remember when choosing the font weights! A weight of 400 is the standard for normal text, a weight of 700 gives you that bold, eye-catching look. And if you’re aiming for an extra bold statement, 900 is your heavy hitter.

@font-face {
  font-family: "Inter";
  src: url("https://fonts.googleapis.com/css2?family=Inter:[email protected]&display=swap");
  font-weight: 100 900; /* Supports a range from thin (100) to extra bold (900) */
}

To use this font in CSS, apply the font-family and font-weight descriptors:

body {
  font-family: "Inter", sans-serif;
  font-weight: 400; /* Normal font weight */
}

font-style

font-style: auto | normal | italic | oblique [ <angle [-90deg, 90deg]>{1, 2}]?;

The font-style descriptor defines whether the font being described should be normal, italic, or oblique. This gives browsers an idea of how the font is supposed to render, so it can be adapted when different styles are applied within your CSS.

The angle is specifically used for creating oblique text. By adding an angle value, you can set how much slant you want, anywhere from -90 to 90 degrees. One angle value gives it a fixed slant, while declaring two angle values allow you to define a range for more variation, which is especially handy for working with variable fonts. Just note that specifying an angle is totally optional.

Using font-style: italic:

@font-face {
  font-family: "Inter";
  src: url("https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/files/inter-latin-italic.woff2") format("woff2");
  font-style: italic;
}

Using font-style: oblique with a degree range:

@font-face {
  font-family: "Inter";
  src: url("https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/files/inter-latin-wght-normal.woff2") format("woff2");
  font-style: oblique -10deg 20deg;
}

font-display

font-display: auto | block | swap | fallback | optional;

Ever been on a site and noticed the text doing a weird flicker thing or just disappearing for a sec? Well, that’s where font-display steps in, it tells the browsers how the font is displayed depending on when it is downloaded and ready to use.

  • auto: The font display is set by the browser.
  • swap: Think of this as having a backup plan. A stand-in font is used, and then your custom font is displayed when ready.
  • block: The text hangs back until your font is fully loaded, so there’s no flickering, but you might have a teensy delay.
  • fallback: It starts with a backup font and will only switch to the desired font if it can be loaded fast enough.
  • optional: The font is only used if it can be loaded immediately. Otherwise, the browser may choose to abort the font download, or download it with a very low priority.
@font-face {
  font-family: "Inter";
  src: url("https://fonts.googleapis.com/css2?family=Inter:[email protected]&display=swap") format("woff2");
  font-display: swap;
}

Along font-display, one of the best practices for font loading optimization is using the <link rel="preload"> to hint the browser on resources that need faster loading.

<link rel="preload" href="/fonts/OpenSans-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous" />

size-adjust

size-adjust: <percentage [0, ∞]>;

With size-adjust, we can scale a font’s size to match other fonts in a fallback stack better. To give you an idea, setting it to 110% makes the font appear slightly more prominent.

@font-face {
  font-family: "Inter";
  src: url("https://fonts.googleapis.com/css2?family=Inter:[email protected]&display=swap") format("woff2");
  size-adjust: 110%;
}

font-feature-settings

font-feature-settings: normal | <feature-tag-value>#;

We use this for some fancy typographic features. For example, liga refers to ligatures, which connect certain letter pairs for a smoother text flow. Meanwhile, the kern feature adjusts spacing between letters to keep everything looking more balanced. But wait, OpenType features have way more cool tricks. Here’s a quick breakdown of some of the available features.

OpenType FeatureFull FormDescription
smcpSmall CapsConverts lowercase into smaller uppercase letters.
c2scCapitals to Small CapsConverts uppercase letters into small caps.
onumOld-Style NumeralsUses non-uniform height numbers.
tnumTabular NumeralsUses equal-width numbers, great for aligning numbers in tables and spreadsheets.
pnumProportional NumeralsUses variable-width numbers.
lnumLining NumeralsUses full-height numbers.
swshSwash GlyphsAdds decorative flourishes to certain letters for an elegant look.
histHistorical FormsUses historical letterforms that were common in older scripts but are not in modern typography.
@font-face {
  font-family: "CustomSerif";
  src: url("CustomSerif.woff2") format("woff2");
  font-feature-settings: "smcp" 1, "liga" 1;
}

font-variation-settings

font-variation-settings: normal | [<string> <number>]#;

The font-variation-settings descriptor gives us fine-grained control over variable fonts. We can use wght to adjust the weight — make text bolder or lighter — and slnt to add a tilt, all without needing to provide separate and multiple font files.

@font-face {
  font-family: "RobotoFlex";
  src: url("RobotoFlex-Variable.woff2") format("woff2");
  font-variation-settings: "wght" 500, "slnt" -10;
}

ascent-override, descent-override and line-gap-override

ascent-override: normal | <percentage [0, ∞]>;
descent-override: normal | <percentage [0, ∞]>;
line-gap-override: normal | <percentage [0, ∞]>;

These descriptors let us adjust how a font aligns with surrounding text. ascent-override modifies height above the baseline, descent-override controls depth below, and line-gap-override fine-tunes spacing between lines for a more balanced look.

@font-face {
  font-family: "Inter";
  src: url("https://fonts.googleapis.com/css2?family=Inter:[email protected]&display=swap") format("woff2");
  ascent-override: 90%;
  descent-override: 110%;
  line-gap-override: 50%;
}

unicode-range

unicode-range: <unicode-range-token>#;

Using unicode-range we can load exactly the characters we need from the font file. For example, if we set a range of U+0025-00FF, then we’re only using the font file’s Latin characters — which is a nice way to download only what is needed and make the font render faster on the page to prevent the FOIT and FOUT issues we looked at earlier.

@font-face {
  font-family: "LatinFont";
  src: url("latin-font.woff2") format("woff2");

  unicode-range: U+0025-00FF; /* Limits the font usage to specified Unicode character range */
}

Font formats and browser compatibility

We can use multiple fonts so the typography works smoothly across various browsers, including browsers that might not support modern font types, like WOFF2 and WOFF. Although these days it is less of a concern since browsers have supported custom fonts for years. Still, it is worth learning about different font file formats, even if it’s for historic purposes, because you are likely to encounter older code that references them.

  • WOFF2: This modern and highly compressed format designed for the web is best for performance. All modern browsers support WOFF2.
  • WOFF: An older compressed format that provides good browser support, used as a fallback for browsers that don’t support WOFF2.
  • TTF/OTF: (TrueType/OpenType). These are traditional desktop font formats. Although they are supported in all browsers, they have larger file sizes. It’s best to use them minimally if needed.
  • EOT: (Embedded OpenType): An outdated format used exclusively for Internet Explorer.
  • SVG: It is a vector-based font format and is primarily used for older iOS devices. I don’t recommend using it at all.
Font FormatSupported BrowsersNotes
WOFF2All major browsersPreferred format for modern web.
WOFFAll major browsersGood fallback for browsers
TTF/OTFAll major browsersUse only if necessary
EOTInternet Explorer onlyNo longer relevant for most use cases
SVGOlder iOS devicesDeprecated, avoid using.

What about variable fonts?

Traditionally, we had to load several font files for different weights and styles. In other words, we might have a normal style, a bold style, and an italic style, and each of those styles needs to be registered as separate @font-face declarations and provided with the correct font file for each style. That’s a lot of work and a lot of code to maintain, especially if you decide to change fonts in the future.

Variable fonts solve this. They’re basically a single font file that supports multiple styles in the same file. This means we can avoid having to declare @font-face for each and every style and keep everything maintained in the same declaration.

To use variable fonts, we stick to the same @font-face syntax. Here, we define a range of values instead of fixed ones, like in the example below, for properties like font-weight and font-style.

@font-face {
  font-family: "RobotoFlex";
  src: url("/fonts/RobotoFlex-VariableFont.woff2") format("woff2");
  font-weight: 100 900; /* Allows any weight between 100 and 900 */
  font-style: normal italic; /* Supports both normal and italic styles */
}

Once we have added the font, we can adjust its properties to fit our design.

h1 {
  font-family: "RobotoFlex", sans-serif;
  font-weight: 700; /* Any value within the defined range */
  font-style: italic;
}

p {
  font-family: "RobotoFlex", sans-serif;
  font-weight: 400; /* Any value within the defined range */
  /* font-style: normal is the default */
}

Common issues and fixes

There is as handful of common issues you are likely to encounter when working with custom fonts and the @font-face at-rule. We’re going to call those out in this section and provide some advice for how to fix them.

Slow-loading fonts

We can use WOFF2, subset fonts, and preload them for fast-loading fonts:

<link rel="preload" href="/fonts/CustomFont.woff2" as="font" type="font/woff2" crossorigin="anonymous" />

Remember, we can also use the unicode-range descriptor to limit which characters are loaded to make the font file easier for the browser to download.

FOIT & FOUT

We’re talking about abbreviations that stand for “flash of invisible text” and “flash of unstyled text” respectively. In other words, the font either takes a hot minute to render before it is displayed, or it renders as the fallback font before it loads in the custom font file. Either way, no bueno!

We can prevent invisible or unstyled text using the font-display property.

@font-face {
  font-family: "CustomFont";
  src: url("/fonts/CustomFont.woff2") format("woff2");
  font-display: swap;
}

Blurry fonts

Some font file are designed with vectors that result in crisp, sharp edges no matter how far we zoom into the text. Other font files are not, and this can result in blurry characters.

The best fix for this is to switch your font file for a modern one, such as .woff2 or .woff. However, if you are stuck with a lower-quality font, you could use the font-smoothing, like this:

body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

Just note that those are vendor-prefixed properties, meaning they are not official CSS properties, but rather ones designed for specific browsers.

Demo

Specification

You can find the latest specification for the @font-face at-rule in the CSS Fonts Module Level 4 and CSS Fonts Module Level 5. Both are currently in Editor’s Draft.

Browser support