CSS@font-face

@font-face: Web Fonts Done Right

@font-face teaches the browser about a font file it can download and use. Getting it working takes four lines; getting it fast — no invisible text, no jarring layout shifts, no 300 KB of glyphs the page never renders — is where the craft lives. This page covers the full descriptor set, font-display strategies, subsetting with unicode-range, variable fonts, and preloading.

Full Syntax

CSS
@font-face {
  /* The name you will reference in font-family */
  font-family: "Inter";

  /* Where to get it — order sources by preference */
  src:
    local("Inter"),                              /* installed copy, zero download */
    url("/fonts/inter.woff2") format("woff2");

  /* Which styles this file covers */
  font-weight: 400;
  font-style: normal;

  /* Loading behavior */
  font-display: swap;

  /* Which characters this file provides */
  unicode-range: U+0000-00FF;
}

/* Then use it like any font */
body {
  font-family: "Inter", system-ui, sans-serif;
}
One rule per style — unless variable
With static fonts you write one @font-face block per weight/style combination (regular, bold, italic…). If a weight is used without a matching block, the browser fakes it (synthetic bold / oblique), which usually looks worse.
Which Format? woff2. Just woff2.

In the past, src lists contained eot, ttf, woff, and svg fallbacks. Today WOFF2 is supported by every browser in use and compresses ~30% better than WOFF. Unless you must support truly ancient browsers, ship woff2 only.

Format

Compression

Support

Verdict

woff2

Brotli — best

All modern browsers

Use this

woff

zlib — good

Universal, incl. very old

Fallback only if needed

ttf / otf

None

Universal

Never ship raw

eot, svg fonts

IE / legacy iOS only

Dead — do not ship

font-display: FOUT, FOIT, and Your Choice

While a font downloads, the browser must decide what to show. Two classic failure modes have names: FOIT (Flash of Invisible Text — text hidden until the font arrives) and FOUT (Flash of Unstyled Text — fallback shown first, then swapped). font-display lets you pick the trade-off explicitly.

Value

Block period

Swap period

Behavior

auto

Browser default

Browser default

Usually like block — avoid

block

~3s invisible

Infinite

FOIT; text hidden up to 3s

swap

~0ms

Infinite

FOUT; fallback shown instantly, swaps whenever font lands

fallback

~100ms

~3s

Brief invisibility; if font is late, fallback stays

optional

~100ms

None

Font used only if (nearly) instant, else fallback for this visit

CSS
/* Content sites: text must be readable immediately */
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter.woff2") format("woff2");
  font-display: swap;
}

/* Performance purist: no layout shift, font is a bonus */
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter.woff2") format("woff2");
  font-display: optional;
}
Which strategy?
swap for body text on content sites,optional when you would rather skip the custom font than shift layout, and block only for icon fonts — where fallback glyphs would render as nonsense boxes.

To reduce the FOUT jolt with swap, tune the fallback with the size adjustment descriptors so the swap barely moves anything:

CSS
/* A metric-compatible fallback that minimizes layout shift */
@font-face {
  font-family: "Inter-fallback";
  src: local("Arial");
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

body {
  font-family: "Inter", "Inter-fallback", sans-serif;
}
unicode-range: Ship Only the Glyphs You Need

unicode-range declares which characters a font file covers. The browser downloads the file only if the page actually uses a character in that range — this is how Google Fonts splits fonts into latin, latin-ext, cyrillic, and greek subsets.

CSS
/* Latin subset — downloaded for most English pages */
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2013-2014,
                 U+2018-201A, U+201C-201E, U+2026;
}

/* Cyrillic subset — only fetched if the page contains cyrillic text */
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-cyrillic.woff2") format("woff2");
  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1;
}

A full multilingual font can exceed 300 KB; a latin subset is often 15–30 KB. Subsetting is the single biggest font optimization available. Tools: glyphhanger, pyftsubset (fonttools), or simply using the subset files a font provider ships.

Variable Fonts

A variable font packs a continuous range of weights (and often widths or slants) into one file. One @font-face rule replaces six, and you unlock every in-between value.

CSS
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-var.woff2") format("woff2-variations");
  font-weight: 100 900;   /* a RANGE, not a single value */
  font-style: normal;
  font-display: swap;
}

h1 { font-weight: 780; }        /* any value in the range */
.subtle { font-weight: 350; }

/* Fine-grained axis control */
.display {
  font-variation-settings: "wght" 650, "slnt" -6;
}
Note
One variable font file is usually smaller than three static weights combined — and cheaper than four. If your design uses two or fewer weights, static subsets may still win. Measure.
Preloading Fonts

Fonts are late-discovered resources: the browser only requests one after it has downloaded the CSS and found an element that needs it. preload short-circuits that chain for your critical font.

HTML
<head>
  <!-- crossorigin is REQUIRED even for same-origin fonts -->
  <link
    rel="preload"
    href="/fonts/inter-var.woff2"
    as="font"
    type="font/woff2"
    crossorigin
  />
  <link rel="stylesheet" href="/styles/main.css" />
</head>
Preload sparingly
Every preloaded font competes with render-blocking CSS for bandwidth. Preload one or two fonts used above the fold — preloading five fonts makes everything slower. And a missing crossoriginattribute causes a silent double download.
A Production-Ready Setup

CSS
/* fonts.css — self-hosted, subset, variable, swap */
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-var-latin.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+2013-2014, U+2018-201A, U+201C-201E;
}

:root {
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
}

body { font-family: var(--font-sans); }
Self-host
Since browsers partitioned their HTTP caches per site (2020+), a Google-hosted font is never "already cached from another site". Self-hosting removes a third-party connection and gives you cache and subsetting control.
Key Takeaways
  • Ship woff2 only; one @font-face per weight/style, or one variable-font rule with a weight range.

  • Set font-display explicitly: swap for content, optional for zero layout shift, block only for icon fonts.

  • Subset with unicode-range — the browser downloads a file only when the page uses its characters.

  • Preload at most one or two critical fonts, always with the crossorigin attribute.

  • Use size-adjust and metric overrides on a local fallback to tame FOUT layout shift.

  • Self-host your fonts — cross-site font caching no longer exists.