Fonts and Web Fonts
Introduction
Fonts strongly influence readability, tone, and brand identity. A good font setup is not only about appearance, but also fallback behavior, loading speed, and user experience under slow networks. This chapter covers practical font-family strategy, system font stacks, web font loading, and performance-minded defaults.
Prerequisites
- Basic CSS syntax and text styling
- A page with an external stylesheet
- Basic understanding of browser loading behavior
Font Families in CSS
Use font-family to define “try this font first; if missing, try the next one”:
body {
/* Try left to right; sans-serif at the end is the generic fallback so text always shows */
font-family: Inter, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}Meaning: the browser walks this list—use Inter if available; otherwise try Segoe UI… and finally fall back to sans-serif (a generic sans-serif family).
How fallback works:
- Browser tries the first font
- If unavailable, it tries the next one
- It ends with a generic family (
sans-serif,serif,monospace, etc.)
Always include a generic family at the end. Wrap font names that contain spaces in quotes, for example "Segoe UI".
Generic Font Families
Common generic families:
serif: serif typefaces (stroke ends have small “feet”; more print-like)sans-serif: sans-serif typefaces (most common for modern UI)monospace: fixed-width characters (great for code)cursive: handwriting / script stylesfantasy: decorative typefacessystem-ui: prefer the operating system’s default UI font
For most modern UI work:
- body/UI text often uses
sans-seriforsystem-ui - code blocks use
monospace
System Font Stack
A system stack prefers fonts already installed on the device, so you usually avoid extra downloads and get better performance:
body {
/* system-ui / -apple-system: follow the OS UI font; then common platform fallbacks */
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}Benefits:
- fast first render
- native platform feel
- fewer network dependencies
Trade-off:
- typography differs slightly across operating systems (Windows / macOS / Android may not look identical)
Web Fonts with @font-face
When you need a specific brand typeface, use @font-face to tell the browser: “this font name maps to that font file.”
@font-face {
font-family: "Inter"; /* custom name you will use later in font-family */
src: url("/fonts/inter-regular.woff2") format("woff2"); /* file path + format */
font-weight: 400; /* this file is the regular weight */
font-style: normal; /* upright, not italic */
font-display: swap; /* show fallback text first; swap in the web font when ready */
}
body {
/* prefer the registered Inter; otherwise fall back to system fonts */
font-family: "Inter", system-ui, sans-serif;
}Key fields:
font-family: custom font name used in stylessrc: font file path and formatfont-weight/font-style: which weight/style this file representsfont-display: what to show while loading
How to Read the src Line
For example:
src: url("/fonts/inter-regular.woff2") format("woff2");Break it down:
| Part | Meaning |
|---|---|
src | source — where the font comes from |
url("/fonts/inter-regular.woff2") | file location: fonts/inter-regular.woff2 at the site root |
format("woff2") | tells the browser this is woff2; use it only if supported |
The filename inter-regular is usually just a convention: the regular version of Inter. The real weight binding comes from font-weight: 400 next to it.
Think of it as two steps: register the font with @font-face, then use it with font-family.
Why woff2 Is Preferred
woff2 is the modern default format for web fonts:
- better compression
- broad support in modern browsers
- smaller download size than many older formats
If legacy browser support is critical, teams may add additional formats, but many modern projects ship woff2 only.
font-display and Perceived Performance
font-display controls what users see before the font is fully loaded.
Common value:
@font-face {
font-family: "Inter";
src: url("/fonts/inter-regular.woff2") format("woff2");
font-display: swap; /* show fallback text first; replace with the web font when ready */
}Behavior:
- browser shows fallback text immediately
- swaps to the web font when ready
This usually avoids the “space reserved but no visible text” problem during loading.
FOIT and FOUT
Two common font-loading effects:
- FOIT (Flash of Invisible Text): text stays hidden until the font loads
- FOUT (Flash of Unstyled Text): fallback font appears first, then switches
Most products prefer a controlled FOUT (with font-display: swap) over FOIT, because readable text appears sooner.
Weight and Style Variants
If your UI uses multiple weights, provide a matching font file for each weight (do not declare only 400 and expect a perfect 600 to appear magically):
@font-face {
font-family: "Inter";
/* url(...) = font file path; format("woff2") = the file format is woff2 */
src: url("/fonts/inter-600.woff2") format("woff2");
font-weight: 600; /* this file maps to weight 600 */
font-style: normal;
font-display: swap;
}What this src line means:
- download the font from
/fonts/inter-600.woff2 inter-600usually means Inter at weight 600 (semi-bold)format("woff2")declares the format so the browser can decide whether to use it
Later, when you write font-weight: 600, the browser uses this file. If a requested weight is missing, browsers synthesize or substitute, which may look inconsistent.
Google Fonts and Hosted Fonts
You can load fonts from providers (for example Google Fonts) or self-host files.
Provider-hosted advantages:
- easy setup
- automatic delivery updates
Self-hosted advantages:
- full control over caching and privacy
- fewer third-party dependencies
- easier performance tuning in some deployments
Choose based on product requirements and infrastructure constraints.
Font Loading and Performance Guidelines
- limit font families (often 1-2 is enough)
- limit number of weight variants
- prefer
woff2 - use
font-display: swap - avoid loading decorative fonts for large body content
- test on slower networks
Typography quality is important, but payload size matters too.
Tip
Keep Typography Simple
A clean hierarchy with one primary family and a small weight scale often looks better than mixing many fonts.
Practical Example: UI + Code Font
A common combo is sans-serif/system fonts for UI text and monospace for code:
body {
/* body copy, buttons, navigation, and other UI text */
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
}
code,
pre {
/* code inside code/pre: monospace so characters align cleanly */
font-family: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
}This keeps UI text readable and code rendering stable.
Common Font Mistakes
| Mistake | Why it hurts | Better approach |
|---|---|---|
| No fallback fonts | Broken look if custom font fails | Always provide a fallback stack |
| Too many families/weights | Slower loading and visual noise | Keep the font system minimal |
Missing font-display | Risk of invisible text | Use font-display: swap |
| Using decorative font for body copy | Lower readability | Reserve decorative fonts for accents |
| Not testing real devices | Hidden rendering issues | Test desktop + mobile + slow network |
Mini Exercise
- Define a system font stack for body text
- Add a monospace stack for
codeandpre - Add one self-hosted
@font-facedeclaration (if font files are available) - Set
font-display: swap - Test behavior with DevTools network throttling
- Compare readability before and after
FAQ
Should I always use custom web fonts?
No. System fonts are often a great default for performance and readability.
Is one font family enough for a full site?
Usually yes. Many high-quality products use one UI family plus one monospace family for code.
Why does text “jump” after load?
That is often fallback-to-web-font swapping. It can be reduced with better fallback metrics and careful font choices.
Can I use only Google Fonts links and skip @font-face?
Yes, but self-hosting gives more control over performance, caching, and third-party dependency.
Do I need many font weights?
Not usually. A minimal scale (for example 400, 600, 700) is enough for most interfaces.