· Workflow · 8 min read
How to Self-Host a Custom Font with @font-face
Serve your own font files: get a WOFF2, write one @font-face rule per style, set caching and CORS headers, and check that the right file loads.
Self-hosting a font means the font files live on your own site and your CSS points at them with @font-face. You decide which files ship, when they change, and how long browsers cache them, and visitors make no extra connection to a font service.
This guide walks through the whole setup: getting a web-ready file, writing the CSS, serving the files with the right headers, and checking that the browser actually uses them.
Is self-hosting faster?
Not automatically. Browsers used to share one HTTP cache across sites, so a popular font from a public CDN might already be on a visitor’s device. That stopped being true when browsers began partitioning the cache by site; Chrome explains the change in Gaining security and privacy by partitioning the cache. Today a font is cached per site no matter who hosts it.
web.dev’s Best practices for fonts is careful here: self-hosting removes a third-party connection, but whether it is faster in practice depends on your setup, such as serving from a CDN over HTTP/2. Measure both if speed is the deciding factor. Self-hosting still gives you control, a stable file that will not change under you, and one fewer third party in your privacy notice.
Before you start: check the license
A font’s license decides whether you may put it on a website, how many visitors are covered, and whether you may convert it. Converting a font to another format does not change what the license allows. Read the license that came with the font, or ask the place you got it from. If you are not sure what a file contains, check it before you ship it.
Step 1: Get a WOFF2 file
Browsers can load TTF and OTF files, but WOFF2 is built for the web. It wraps the same font data with much better compression; web.dev says WOFF2 “compresses 30% better than WOFF”, which itself is smaller than an uncompressed TTF or OTF. The same web.dev article quotes the advice to use only WOFF2, since every current browser supports it. WOFF2, WOFF, TTF, or OTF? covers the format question in more detail.
If you have a desktop font, convert it in your browser with the TTF to WOFF2 or OTF to WOFF2 converter. The file is processed on your device and not uploaded.
You need one file per weight and style you plan to use, for example Regular, Bold, and Italic. A variable font is the exception: one file can cover a whole range of weights.
Step 2: Put the files in your project
Keep fonts in their own folder with simple, lowercase names:
public/
fonts/
brand-sans-regular.woff2
brand-sans-bold.woff2
brand-sans-italic.woff2
css/
fonts.css“Brand Sans” is a placeholder; use your font’s name. Many build tools add a content hash to file names (brand-sans-regular.3f9a1c.woff2). That is useful, because it lets you cache the files for a long time, as shown in step 5.
Step 3: Write one @font-face rule per file
Each @font-face rule tells the browser where one file is and which weight and style it provides:
/* css/fonts.css */
@font-face {
font-family: 'Brand Sans';
src: url('../fonts/brand-sans-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Brand Sans';
src: url('../fonts/brand-sans-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Brand Sans';
src: url('../fonts/brand-sans-italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}What each line does (MDN’s @font-face reference has the full list):
font-familyis the name you will use in your CSS. It does not have to match the name inside the font file, but every weight and style of one family should share it.srcis the file’s URL. In a stylesheet, relative URLs are resolved from the stylesheet’s location, not the page’s.format('woff2')lets the browser skip a source it cannot use without downloading it.font-weightandfont-styledescribe the file. When an element asks forfont-weight: 700, the browser picks the Bold file because of these lines.font-displaydecides what visitors see while the font downloads.swapshows fallback text right away and switches when the font arrives. Choosing a font-display value explains the other options.
Browsers only download a face when the page uses it. If no text on the page is italic, the italic file is not fetched.
Step 4: Use the font
Reference the family name with a fallback stack, and use normal CSS weights and styles:
body {
font-family: 'Brand Sans', system-ui, sans-serif;
}
h1,
h2,
strong {
font-weight: 700;
}If you ask for a weight or style you did not declare, for example font-weight: 600 with only 400 and 700 files, the browser picks the closest available face. If you ask for bold or italic and no such face exists at all, the browser may draw a synthesized (“faux”) bold or slanted version, which usually looks worse than the real thing. You can turn that off with font-synthesis:
body {
font-synthesis: none;
}Variable fonts
A variable font covers a range of weights in one file (Variable fonts on the web goes deeper). Declare the range instead of a single number, as described in MDN’s font-weight descriptor page:
@font-face {
font-family: 'Brand Sans';
src: url('../fonts/brand-sans-variable.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}Use the range the font really supports. The font viewer lists a variable font’s axes with their minimum, maximum, and default values; the weight axis is tagged wght.
Step 5: Serve the files with the right headers
Three things matter on the server.
Content type. WOFF2 files should be sent as font/woff2, the media type registered in RFC 8081. Most hosts do this already; check the Content-Type response header in your browser’s developer tools.
Caching. Fonts rarely change, so cache them for a long time. If your file names include a content hash, a new version gets a new name and you can cache the old one for a year:
# Netlify or Cloudflare Pages: a _headers file in the published folder
/fonts/*
Cache-Control: public, max-age=31536000, immutable# nginx
location /fonts/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}Only use immutable if the file name changes whenever the file changes. Otherwise pick a shorter max-age so visitors eventually get updates. MDN’s Cache-Control page explains each directive.
CORS for other domains. Browsers fetch fonts in CORS mode. If the fonts are on the same origin as the page, you need nothing extra. If they are on another domain, such as static.example.com or a separate CDN host, that server must allow your site with an Access-Control-Allow-Origin header, or the font will be blocked:
Access-Control-Allow-Origin: https://www.example.comSee MDN’s CORS guide for the details.
Step 6: Decide whether to preload
Fonts are discovered late: the browser has to download and apply your CSS before it knows which font files it needs. For the one or two fonts used in the first screen, a preload hint can start the download sooner. Preloading every font usually makes things slower, not faster. How to preload web fonts covers when it helps and the exact markup.
Check that it worked
- Network panel. Reload with developer tools open and filter by “Font”. You should see only the WOFF2 files the page uses, each with status 200 (or served from cache on a second load) and
Content-Type: font/woff2. - Rendered font. In Chrome, select an element and open the Computed tab; “Rendered Fonts” at the bottom shows which font actually drew the text. Firefox has a Fonts panel in the Inspector.
- Console. CORS errors and failed font loads show up here.
Common mistakes
- Wrong path. The
url()is relative to the CSS file. An absolute path such as/fonts/brand-sans-regular.woff2avoids surprises. - A different family name for each weight. Declaring
'Brand Sans Bold'as its own family meansfont-weight: 700on'Brand Sans'finds no bold face. Use one family name and letfont-weightchoose the file. - Weights that do not match the files. If the Bold file is declared as
font-weight: 400, the browser uses it for regular text. Check each file’s weight in the font viewer before writing the rule. - No fallback fonts. Always end the stack with a generic family such as
sans-seriforserif.
A shortcut: the FontPivot web font kit
When you convert to WOFF2 with the font converter or the font viewer, you can download a web font kit. Both tools make the same kit. The ZIP contains a fonts/ folder with the WOFF2 file; a style.css with an @font-face rule like the ones above, using the name, weight, and style read from the font, followed by adjusted fallback font rules; a README.md with the CSS and snippets for preload tags, Next.js, Tailwind, and Astro; and an index.html sample page. Read the font-weight and font-style lines before you copy the rule into your project, and adjust the url() to where you put the file.
Further reading
- MDN: Web fonts
- web.dev: Best practices for fonts
- W3C: CSS Fonts Module Level 4