CSS Typography

Как использовать @font-face: пошаговое руководство

Updated Февраль 24, 2026
Пошаговое руководство по использованию CSS @font-face для загрузки пользовательских шрифтов — синтаксис, выбор формата и практические примеры для копирования.

How to Use @font-face: Step-by-Step

The @font-face rule is the foundational mechanism that makes custom web typography possible. Before it existed, designers were stuck with a narrow band of system fonts — Times New Roman, Arial, Georgia — fonts that happened to be pre-installed on most operating systems. Today, @font-face lets you load virtually any typeface directly from your server or a CDN, giving you complete typographic control over how your content appears regardless of what a visitor has installed. This tutorial walks you through the complete process from a blank stylesheet to a fully functional custom font family.

What @font-face Does

At its core, @font-face is a CSS at-rule that maps a font name you define to an actual font file hosted somewhere on the web. When the browser encounters a CSS declaration like font-family: "MyFont", it consults the registered @font-face rules to find the corresponding file, downloads it, parses it, and uses it to render text. Without this rule, the browser would fall back to the next font in the stack or to a system default.

The web font ecosystem has matured enormously since @font-face was first introduced in CSS2 (and then re-standardized in CSS3 after years of inconsistent browser support). Today, the rule is supported universally across all modern browsers, and the tooling around it — format converters, subsetting utilities, CDN delivery — has become deeply sophisticated.

Understanding what happens under the hood helps you use @font-face more effectively. When a page loads, the browser parses HTML and CSS, builds the render tree, and identifies which font families are actually needed for the visible text. Only then does it begin downloading the font files. This lazy loading behavior is efficient, but it introduces the risk of a FOUT (Flash of Unstyled Text) or FOIT (Flash of Invisible Text) during the download window. The font-display descriptor, covered later, is specifically designed to manage this trade-off.

The @font-face rule is also how the browser distinguishes between different variants of the same typeface — bold, italic, light, condensed. Each weight and style combination typically maps to a separate font file, and each gets its own @font-face block. This matters because if you declare only a regular weight but use font-weight: 700 in your CSS, the browser will synthesize a fake bold by algorithmically thickening the letterforms. Synthesized bold and italic almost always look worse than purpose-drawn variants. Loading the actual files prevents this.


The Basic Syntax

The simplest possible @font-face rule has two required parts: a font-family name and a src pointing to a font file.

@font-face {
  font-family: "MyFont";
  src: url("/fonts/myfont.woff2") format("woff2");
}

The font-family value is a string you invent — it doesn't need to match the actual font name, though using the real name avoids confusion. This name becomes the identifier you use throughout the rest of your CSS: font-family: "MyFont", sans-serif.

The src descriptor is where the real work happens. It accepts one or more url() functions, each optionally followed by a format() hint. The format() hint tells the browser what kind of file to expect before downloading it, which lets browsers that don't support a given format skip it immediately rather than downloading and failing to parse it. For WOFF2 files, the format string is "woff2". For older WOFF1, it's "woff". For TrueType files, use "truetype".

You can provide multiple source entries as a comma-separated list, and the browser will pick the first one it can use:

@font-face {
  font-family: "MyFont";
  src: url("/fonts/myfont.woff2") format("woff2"),
       url("/fonts/myfont.woff") format("woff"),
       url("/fonts/myfont.ttf") format("truetype");
}

In 2024, supporting all three formats is largely unnecessary — WOFF2 has near-universal support. But for applications that must support very old browsers, the fallback chain is still valid. More on format selection below.

The local() function is another useful src option. It tells the browser to check whether the font is already installed on the user's system before reaching out to the network:

@font-face {
  font-family: "Inter";
  src: local("Inter"),
       url("/fonts/inter.woff2") format("woff2");
}

For Inter, a font installed on many modern systems, this check can eliminate a network request entirely. However, local() has a subtle privacy implication — it can be used to fingerprint users by detecting which fonts they have installed. For that reason, some security-conscious developers omit it.


Adding Multiple Weights and Styles

A complete font family declaration requires one @font-face block per variant. The font-weight and font-style descriptors tell the browser which variant each block represents, so the browser knows which file to load when a CSS rule calls for bold or italic text.

/* Regular */
@font-face {
  font-family: "Roboto";
  src: url("/fonts/roboto-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
}

/* Bold */
@font-face {
  font-family: "Roboto";
  src: url("/fonts/roboto-bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
}

/* Italic */
@font-face {
  font-family: "Roboto";
  src: url("/fonts/roboto-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
}

/* Bold Italic */
@font-face {
  font-family: "Roboto";
  src: url("/fonts/roboto-bold-italic.woff2") format("woff2");
  font-weight: 700;
  font-style: italic;
}

All four blocks use the same font-family name — "Roboto" — which is what ties them together as a family from the browser's perspective. When your CSS says font-family: "Roboto" and font-weight: 700, the browser matches that against the block that declares font-weight: 700 and downloads the corresponding file.

Font weight values follow the standard numeric scale: 100 (Thin), 200 (ExtraLight), 300 (Light), 400 (Regular), 500 (Medium), 600 (SemiBold), 700 (Bold), 800 (ExtraBold), 900 (Black). Not all typefaces include all nine weights — a typical workhorse font like Roboto ships with 12 variants (six weights × two styles), while a display face might offer only Regular and Bold.

Variable Fonts and Range Declarations

For variable fonts, the font-weight descriptor accepts a range instead of a single value:

@font-face {
  font-family: "InterVariable";
  src: url("/fonts/inter-variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
}

This single declaration covers the entire weight axis of the variable font — one file, unlimited weights. If you're working with variable fonts, the variable fonts complete guide covers the full set of axis controls.


Font Format: Why WOFF2 Is All You Need

The history of web font formats is a story of competing standards gradually giving way to a single clear winner. Early web fonts relied on EOT (Embedded OpenType), a Microsoft-specific format. Then came TTF and OTF (TrueType and OpenType) served raw over the web, followed by WOFF (Web Open Font Format) in 2010, which added compression and origin restrictions. WOFF2, standardized in 2018, brought significantly better compression through the Brotli algorithm.

WOFF2 delivers approximately 30% smaller file sizes compared to WOFF1, and 60-70% smaller than uncompressed TTF files. Given that font file size directly affects page load time and therefore user experience, this compression advantage is substantial. A typical WOFF2 file for a single font weight might be 20-50 KB, where the equivalent raw TTF could be 80-200 KB.

Browser support for WOFF2 is now essentially universal — it covers 98%+ of global web traffic as of 2024. The only browsers that lack WOFF2 support are Internet Explorer 11 and below, which account for a fraction of a percent of traffic in most analytics. Unless your specific audience data shows a meaningful IE11 user base, you can safely drop WOFF and TTF fallbacks entirely:

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
}

This is cleaner, simpler, and eliminates the overhead of listing fallback formats. The format("woff2") hint ensures that any rare browser that doesn't understand the format will skip the declaration and fall back to the next font in your font-family stack.

Subsetting for Maximum Performance

One of the most effective optimizations for @font-face fonts is subsetting — including only the Unicode characters you actually need. Google Fonts does this automatically by detecting the language of the page. If you're self-hosting, tools like pyftsubset (from FontTools), glyphhanger, or the online Subsetter at Font Squirrel let you generate a subset file containing only Latin characters, for instance, rather than the full Unicode range.

The unicode-range descriptor in @font-face takes subsetting further — it lets you specify which character ranges a given font file covers, so the browser only downloads the file when those characters are actually present on the page:

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-latin.woff2") format("woff2");
  font-weight: 400;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}

For more detail on the subsetting workflow, see the font subsetting guide.


Setting font-display for Better UX

The font-display descriptor controls what happens during the window between when the browser starts loading a font and when it finishes. Without it, different browsers apply their own defaults — Chrome historically showed invisible text for up to 3 seconds (FOIT), while Firefox showed fallback text immediately (FOUT). The result was unpredictable cross-browser behavior that could significantly impact perceived performance.

font-display accepts five values:

auto — The browser decides. Generally equivalent to block in most browsers, meaning text is invisible while the font loads.

block — A short "block period" (typically 3 seconds) during which text is invisible. If the font loads within that window, it swaps in; otherwise, the browser uses the fallback for the rest of the page load.

swap — No block period. Text is immediately rendered in the fallback font, then swapped to the custom font when it loads. This maximizes text visibility but causes a visible layout reflow when the swap happens.

fallback — An extremely short block period (about 100ms), then fallback text, then a swap window of about 3 seconds. After that, if the font still hasn't loaded, the fallback is kept permanently for that page view.

optional — A very short block period, then the browser decides whether to swap based on connection speed. On slow connections, the custom font may be skipped entirely for that page view.

For most websites, font-display: swap is the right default. It prevents invisible text entirely and ensures users see content immediately — important both for perceived performance and for Core Web Vitals (FOIT can delay the Largest Contentful Paint measurement). The visual "flash" when the custom font loads is usually minor, especially if you choose a fallback font with similar proportions.

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

For design-critical applications where font consistency is paramount — a fashion brand where the typeface is integral to the visual identity — font-display: block or fallback may be more appropriate, accepting the brief invisible text period in exchange for no flash. The font-display strategies guide covers these trade-offs in depth.


Complete Example: Loading a Custom Font Family

Putting it all together: here is a production-ready @font-face setup for Inter, loading four variants (regular, medium, bold, and italic) with WOFF2, proper weight descriptors, and font-display: swap. This is a self-hosted configuration — see the how to self-host Google Fonts guide for instructions on acquiring and organizing the files.

/* Inter: Self-hosted, production-ready @font-face setup */

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

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

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

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

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

/* Usage */
body {
  font-family: "Inter", system-ui, -apple-system, sans-serif;
  font-weight: 400;
  font-style: normal;
}

h1, h2, h3 {
  font-weight: 700;
}

.label {
  font-weight: 500;
}

strong, b {
  font-weight: 600;
}

Notice the fallback stack after "Inter": system-ui, -apple-system, sans-serif. These ensure readable text is shown immediately while Inter loads, and they also serve as the permanent fallback on the rare occasion the font file is unavailable. Choosing a fallback with similar proportions to your primary font minimizes layout shift when the swap occurs.

Preloading Critical Fonts

For the font variant used in your largest visible text block — typically the body font or the main headline weight — adding a <link rel="preload"> tag in the HTML <head> tells the browser to fetch the file earlier in the loading process, before it would normally encounter the @font-face declaration:

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

The crossorigin attribute is required even for same-origin fonts — the browser's font loading process always uses CORS. Without it, the font will be fetched twice. Limit preloading to one or two files: the <link rel="preload"> tag increases page weight by loading fonts unconditionally, even on pages where that particular weight may not be used.

For a comprehensive reference on every @font-face descriptor and advanced usage patterns including unicode-range, size-adjust, and ascent-override, see the complete @font-face guide. The how to import Google Fonts with CSS article covers the alternative path of using Google's CDN rather than self-hosting — a valid choice for many projects that trades fine-grained control for zero-maintenance convenience.

Typography Terms

Try These Tools

Fonts Mentioned

Related Articles