CSS Font Face

The @font-face rule lets you load and use custom font files that you host yourself. Instead of relying on Google Fonts or system fonts, you provide the font files directly — giving you full control over which fonts load, how they load, and how they look, even offline.

Why Self-Host Fonts

  • Works without internet access (intranet sites, offline apps)
  • No third-party dependency — no Google, no external requests
  • Full privacy — Google Fonts can log user IPs; self-hosted fonts cannot
  • Custom or licensed fonts not available on Google Fonts
  • Slightly faster in some setups because the font is on the same server as your site

Basic @font-face Syntax

@font-face{ 
  font-family: "MyFont";         /* Name you'll use in CSS */
  src: url("fonts/myfont.woff2") format("woff2"),
       url("fonts/myfont.woff")  format("woff");
  font-weight: normal;           /* 400 */
  font-style: normal;
  font-display: swap;
 }

After declaring it, use the name just like any other font:

body {
  font-family: "MyFont", sans-serif;
}

Font File Formats

DIAGRAM — Font format support:

Format    │ Extension │ Support
──────────┼───────────┼─────────────────────
WOFF2     │ .woff2    │ All modern browsers ← Use this first
WOFF      │ .woff     │ All modern browsers ← Fallback
TTF/OTF   │ .ttf/.otf │ Most browsers (larger file)
SVG       │ .svg      │ Old iOS Safari only (avoid)
EOT       │ .eot      │ Old IE only (avoid)

Always provide WOFF2 first — it is the most compressed format, so pages load faster. WOFF as a fallback covers any remaining browsers. TTF/OTF is a safe last resort since file sizes are larger.

src Descriptor — Format Stack

The src descriptor lists font sources in order. The browser downloads the first format it supports.

@font-face{ font-display:swap;
  font-family: "BrandFont";
  src: url("fonts/brand.woff2") format("woff2"),
       url("fonts/brand.woff")  format("woff"),
       url("fonts/brand.ttf")   format("truetype");
 }
DIAGRAM — Browser picks the best supported format:

Browser checks: woff2 supported? → YES → downloads brand.woff2, stops.
                woff2 supported? → NO  → woff supported? → YES → uses woff.
                                                         → NO  → tries ttf.

local() — Check System First

Add local() before the URL sources to check if the user's computer already has the font installed. If it does, the browser skips the download entirely.

@font-face{ font-display:swap;
  font-family: "Inter";
  src: local("Inter"),
       local("Inter-Regular"),
       url("fonts/Inter.woff2") format("woff2");
 }
DIAGRAM — local() shortcut:

User has Inter installed? → YES → use system copy (no download)
                          → NO  → download from server

Loading Multiple Weights

Each font weight and style requires a separate @font-face block. Use the same font-family name but different font-weight values.

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

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

/* Italic */
@font-face{ font-display:swap;
  font-family: "BrandFont";
  src: url("fonts/brand-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
 }
/* All three variants now work under one name */
h1 { font-family: "BrandFont"; font-weight: 700; }
p  { font-family: "BrandFont"; font-weight: 400; }
em { font-family: "BrandFont"; font-style: italic; }

Variable Fonts — One File, All Weights

A variable font stores the entire weight range in a single file. This reduces HTTP requests from many to one.

@font-face{ font-display:swap;
  font-family: "BrandFont";
  src: url("fonts/brand-variable.woff2") format("woff2-variations");
  font-weight: 100 900;   /* range this file covers */
  font-style: normal;
 }

/* Now any weight from 100 to 900 works */
h1 { font-weight: 800; }
p  { font-weight: 350; } /* intermediate weights too */

font-display — Control Loading Behavior

DIAGRAM — font-display values and behavior:

swap      → Text visible immediately in fallback font, swaps when loaded
            ✅ Best for body text — readability first

block     → Text invisible for up to 3s, then shows custom or fallback
            ⚠ Can show blank text — use sparingly for decorative fonts

fallback  → Very short invisible period, then fallback. Swaps only if
            font loads within ~3s.
            ✅ Good balance for performance

optional  → Short invisible period. If font not ready, use fallback
            permanently. No swap.
            ✅ Best for performance — font may not show on slow connections

auto      → Browser decides (usually same as block)
@font-face{ 
  font-family: "BrandFont";
  src: url("fonts/brand.woff2") format("woff2");
  font-display: swap; /* recommended for most cases */
 }

Recommended File Folder Structure

project/
├── css/
│   └── style.css
└── fonts/
    ├── brand-regular.woff2
    ├── brand-regular.woff
    ├── brand-bold.woff2
    └── brand-bold.woff
/* In style.css — path is relative to the CSS file */
@font-face{ font-display:swap;
  font-family: "BrandFont";
  src: url("../fonts/brand-regular.woff2") format("woff2");
  font-weight: 400;
 }

Converting Font Formats

If you only have a TTF or OTF file, convert it to WOFF2 and WOFF using free tools like Fontsquirrel's Webfont Generator or the woff2 command-line tool. Always convert legally licensed fonts only.

@font-face vs Google Fonts

Feature@font-face (self-hosted)Google Fonts
Works offlineYesNo
PrivacyFull — no third-party requestsGoogle may log requests
Custom/licensed fontsYesOnly free Google fonts
Setup effortMediumVery easy
CDN deliveryYour serverGoogle's global CDN

Self-hosting fonts with @font-face is the professional approach for branded projects with custom typefaces or strict privacy requirements. Use WOFF2 as your primary format, include a WOFF fallback, and always set font-display: swap to keep text visible while fonts load.

Leave a Comment

Your email address will not be published. Required fields are marked *