CSS Typography

@font-face: Özel Web Fontları İçin Tam Rehber

Updated Şubat 24, 2026
@font-face hakkında her şey — sözdizimi, format seçimi, unicode-range alt kümeleme, font-display stratejileri ve self-hosting ile CDN karşılaştırması.

@font-face: The Complete Guide to Custom Web Fonts

The @font-face rule is how the web loads custom typefaces. Without it, every website would be limited to the small set of fonts pre-installed on users' operating systems. With it, you can serve any typeface you have the right to distribute — giving you complete control over your site's typographic identity.

This guide covers everything you need to know: the syntax, the file formats, how to handle loading behavior, how to scope which characters trigger a download, and when to self-host versus relying on a CDN like Google Fonts.


@font-face Syntax and Descriptors

The @font-face rule is a CSS at-rule that maps a font name (that you define) to one or more font files. The browser uses this mapping to download and render custom typefaces.

Here is the full syntax with all available descriptors:

@font-face {
  /* Required */
  font-family: 'My Custom Font';
  src: url('/fonts/my-font.woff2') format('woff2'),
       url('/fonts/my-font.woff')  format('woff');

  /* Descriptors (all optional) */
  font-weight:  400;
  font-style:   normal;
  font-stretch: normal;
  font-display: swap;
  unicode-range: U+0000-00FF;
  font-named-instance: 'Regular';

  /* Advanced: OpenType feature settings */
  font-feature-settings: normal;
  font-variation-settings: normal;
}

The font-family descriptor names the font — this is what you reference in your font-family property elsewhere in your CSS. It doesn't need to match the actual name of the font file; it's just an alias. That said, using the real name is good practice for clarity.

The src descriptor is where you list the font files. The browser works through the list in order and uses the first format it supports. Separate each source with a comma. The format() hint is technically optional but strongly recommended — it lets the browser skip downloading files it can't use without making a network request first.

When you have multiple weights or styles of the same typeface, you define a separate @font-face block for each variant:

/* Regular */
@font-face {
  font-family: 'Lato';
  src: url('/fonts/Lato-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* Bold */
@font-face {
  font-family: 'Lato';
  src: url('/fonts/Lato-Bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* Italic */
@font-face {
  font-family: 'Lato';
  src: url('/fonts/Lato-Italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

/* Bold Italic */
@font-face {
  font-family: 'Lato';
  src: url('/fonts/Lato-BoldItalic.woff2') format('woff2');
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}

The browser matches font-weight, font-style, and font-stretch descriptors to know which file to load for a given combination of properties.


Font Formats: WOFF2, WOFF, TTF, OTF

Over the years, several font formats have been used on the web. Today, the landscape is much simpler than it once was.

WOFF2 (Web Open Font Format 2)

WOFF2 is the format you should use for production. It uses Brotli compression, which makes files roughly 30% smaller than WOFF. All modern browsers have supported it since 2016. For the vast majority of projects, WOFF2 alone is sufficient.

@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-Regular.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

WOFF (Web Open Font Format 1)

WOFF uses zlib compression and was widely supported before WOFF2. If you need to support Internet Explorer 11 or very old mobile browsers, include WOFF as a fallback. Otherwise, skip it.

/* WOFF2 first, WOFF as fallback for older browsers */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-Regular.woff2') format('woff2'),
       url('/fonts/Inter-Regular.woff')  format('woff');
  font-weight: 400;
  font-display: swap;
}

TTF and OTF (TrueType and OpenType)

TrueType (.ttf) and OpenType (.otf) are the native font formats used by desktop operating systems. They are uncompressed, making them significantly larger than WOFF2. You should not serve these formats over the web unless you have a specific reason — the file size penalty is too high.

The one scenario where TTF makes sense in web contexts: as a source file that you convert to WOFF2 using a tool like woff2_compress (Google's command-line converter) or an online service like Transfonter.

A Note on EOT and SVG Fonts

Embedded OpenType (.eot) was an Internet Explorer format and is now obsolete. SVG fonts were used by very early iOS Safari and are completely dead. You will sometimes see these in old @font-face CSS — there is no reason to include them today.

The Modern Production Format Recommendation

/* For 2026 and beyond — WOFF2 only for modern projects */
@font-face {
  font-family: 'Source Serif 4';
  src: url('/fonts/SourceSerif4-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* If you must support IE11 — add WOFF fallback */
@font-face {
  font-family: 'Source Serif 4';
  src: url('/fonts/SourceSerif4-Regular.woff2') format('woff2'),
       url('/fonts/SourceSerif4-Regular.woff')  format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

unicode-range: Load Only What You Need

The unicode-range descriptor tells the browser which Unicode characters this @font-face block covers. This enables a powerful optimization: the browser only downloads a font file if the page actually contains characters in the specified range.

/* Latin characters only */
@font-face {
  font-family: 'Roboto';
  src: url('/fonts/roboto-latin.woff2') format('woff2');
  font-weight: 400;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Latin Extended for languages like Polish, Turkish */
@font-face {
  font-family: 'Roboto';
  src: url('/fonts/roboto-latin-ext.woff2') format('woff2');
  font-weight: 400;
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Greek */
@font-face {
  font-family: 'Roboto';
  src: url('/fonts/roboto-greek.woff2') format('woff2');
  font-weight: 400;
  unicode-range: U+0370-03FF;
}

/* Cyrillic */
@font-face {
  font-family: 'Roboto';
  src: url('/fonts/roboto-cyrillic.woff2') format('woff2');
  font-weight: 400;
  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

/* Cyrillic Extended */
@font-face {
  font-family: 'Roboto';
  src: url('/fonts/roboto-cyrillic-ext.woff2') format('woff2');
  font-weight: 400;
  unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF,
                 U+A640-A69F, U+FE2E-FE2F;
}

This is the exact technique Google Fonts uses. When you load Roboto or Inter from Google Fonts, the CSS they serve contains multiple @font-face blocks, each covering a different script subset. An English-only page downloads only the Latin subset; a page in Russian downloads the Cyrillic subset as well.

This approach is especially powerful for large CJK (Chinese, Japanese, Korean) fonts that can be hundreds of kilobytes per weight:

/* Japanese subset — only characters used on the page */
@font-face {
  font-family: 'Noto Sans JP';
  src: url('/fonts/noto-sans-jp-hiragana.woff2') format('woff2');
  unicode-range: U+3041-3096; /* Hiragana */
}

@font-face {
  font-family: 'Noto Sans JP';
  src: url('/fonts/noto-sans-jp-katakana.woff2') format('woff2');
  unicode-range: U+30A1-30FA; /* Katakana */
}

font-display: Controlling the Loading Experience

The font-display descriptor controls what the browser shows while a custom font is loading. This directly affects the user experience — poorly configured font loading can cause text to be invisible or jump around as fonts swap in.

@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-Regular.woff2') format('woff2');
  font-weight: 400;
  font-display: swap; /* Recommended for most cases */
}

The Five Values

auto — The browser decides, typically behaving like block. Not recommended.

block — The browser shows invisible text for up to 3 seconds (the block period), then shows the fallback. The custom font can still swap in afterward. This creates FOIT (Flash of Invisible Text). Avoid for body text.

@font-face {
  font-family: 'Display Font';
  src: url('/fonts/display.woff2') format('woff2');
  font-display: block; /* Only reasonable for icon fonts where fallback = gibberish */
}

swap — Shows the fallback font immediately, then swaps in the custom font when loaded. This causes FOUT (Flash of Unstyled Text) but ensures users always see text. Best for body copy and most headings.

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

fallback — A compromise: 100ms block period (very brief FOIT), then shows fallback. If the font loads within 3 seconds total, it swaps in. After 3 seconds, no swap happens — the fallback sticks until the next page load. Good for fonts that are cached quickly.

@font-face {
  font-family: 'Lora';
  src: url('/fonts/Lora-Regular.woff2') format('woff2');
  font-display: fallback;
}

optional — 100ms block period, then falls back permanently. The browser may choose not to download the font at all on slow connections. Excellent for non-critical decorative fonts or when you have well-matched fallbacks.

@font-face {
  font-family: 'Playfair Display';
  src: url('/fonts/PlayfairDisplay-Regular.woff2') format('woff2');
  font-display: optional; /* Won't cause layout shift — can be cached silently */
}

For most production sites, font-display: swap is the right default. Pair it with well-matched fallback fonts and size-adjust (see the CSS Font Stacks guide) to minimize visual disruption during the swap.


Self-Hosting vs. Google Fonts CDN

There are two main ways to deliver web fonts: using a CDN like Google Fonts, or self-hosting the files on your own server. Each has trade-offs.

Google Fonts CDN

Google Fonts is the easiest option. Add a <link> to the <head> and you're done.

<!-- Preconnect for faster loading -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- Load Inter, weights 400 and 700 -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">

Advantages: - Zero configuration - Automatic subset delivery - Automatic WOFF2 serving (Google detects browser support) - Google's CDN has excellent global latency

Disadvantages: - Two extra DNS lookups (fonts.googleapis.com and fonts.gstatic.com) - GDPR concerns: the CDN request shares the user's IP address with Google - HTTP/2 push is no longer possible (Chrome dropped cross-site cache sharing) - You're dependent on Google's uptime

Self-Hosting

Self-hosting means downloading the font files and serving them from your own domain.

# Download from Google Fonts Downloader or use google-webfonts-helper
# Then serve from /public/fonts/ or equivalent
/* Self-hosted Inter — all served from your own domain */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
<!-- Preload critical font files -->
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>

Advantages: - No third-party DNS lookups - Full control over caching headers - GDPR-compliant by default (no IP shared with Google) - Fonts are co-located with your other assets, potentially faster on a good CDN

Disadvantages: - Manual setup and file management - You need to update files manually for new font versions - No automatic subsetting

Performance tip: The rel="preload" hint on your critical font files (typically the regular weight of your primary body font) is one of the highest-impact performance optimizations available for typography. It tells the browser to start downloading the font immediately, before it has parsed the CSS.


Local Font Fallbacks with local()

The local() function in a src descriptor tells the browser to check if the font is already installed on the user's system before downloading it. If found locally, no network request is made.

@font-face {
  font-family: 'Inter';
  src: local('Inter Regular'),
       local('Inter-Regular'),
       url('/fonts/Inter-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

You can include multiple local() variations to account for different platform naming conventions (macOS, Windows, and Linux often use different internal font names).

Finding the Correct Local Font Name

The local name can be tricky to get right. You can typically find it by: 1. Installing the font on your system 2. On macOS: opening Font Book, selecting the font, and looking at the PostScript name 3. On Windows: right-clicking the font file → Properties

@font-face {
  font-family: 'Roboto';
  src: local('Roboto'),          /* Used by Android */
       local('Roboto Regular'),  /* PostScript name on macOS */
       url('/fonts/Roboto-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

The Security Consideration

There is a fingerprinting concern with local(): a malicious site could detect whether a user has specific fonts installed by checking if the local fallback was used (via timing side-channels). For this reason, Firefox has moved to restricting local() to system fonts, and Chrome is considering similar restrictions.

For privacy-sensitive applications, or if you're using an unusual font that's unlikely to be pre-installed, skipping local() is reasonable:

/* Simpler — skip local() for better privacy */
@font-face {
  font-family: 'Playfair Display';
  src: url('/fonts/PlayfairDisplay-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

Complete Production Example

Here's a full, production-ready @font-face setup for a site using Inter as its primary font and Lora as a display serif:

/* === Inter — Primary sans-serif === */

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
                 U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-medium.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
                 U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-semibold.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
                 U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
                 U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

/* === Lora — Display serif === */

@font-face {
  font-family: 'Lora';
  src: url('/fonts/lora-regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: optional;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
                 U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Lora';
  src: url('/fonts/lora-italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: optional;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
                 U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

/* === Apply === */
body {
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
}

h1, h2, h3 {
  font-family: 'Lora', Georgia, serif;
}

Using font-display: optional for the display serif is a thoughtful choice here: if the font is already cached (from a previous visit), it loads instantly; if not, the user sees the Georgia fallback and the Lora file is cached silently for next time — with no layout shift at all.

The @font-face rule is one of the most powerful tools in your CSS arsenal. Understanding its descriptors fully — especially unicode-range and font-display — is the difference between a font-loading experience that aids your site and one that hinders it.

CSS Typography Deep Dive

Typography Terms

Try These Tools

Fonts Mentioned

Related Articles