CSS Fonts

Fonts are one of the biggest levers you have over how a page feels. CSS gives you a set of properties — font-family, font-size, font-weight, font-style, and more — plus the font shorthand and the @font-face rule for loading custom typefaces. Getting fonts right means understanding not just which properties exist, but how the browser resolves a font stack, falls back when a font is missing, and computes final pixel sizes from relative units.

This lesson covers the full font system: how the browser picks a typeface, every core font property, the shorthand, loading web fonts with @font-face, and the mistakes that quietly break text rendering across browsers and operating systems.

Overview / How it works

When the browser paints text, it does not just use “the font you asked for.” It builds an ordered list of candidate font families from font-family, then walks that list looking for the first one actually installed on the system or loaded via @font-face. If none of your named fonts are available, it falls back to the browser’s default (usually a serif or sans-serif system font). This is why a font stack, not a single font name, is the correct way to write font-family.

Font properties are all inherited by default. If you set font-family and font-size on <body>, every descendant element inherits those values unless something more specific overrides them. This is deliberate: text-related properties cascade down so an entire document shares a consistent typographic voice, and you only override at the points that need to differ (headings, code blocks, captions).

Sizing is where the box model and typography intersect. font-size does not just control the glyphs — it is the basis for the em unit and (via the root element) the rem unit used throughout the rest of your CSS for padding, margin, and layout. Change the root font-size and every rem-based measurement on the page rescales with it. This is also why relative units (em, rem, %, and viewport-based functions like clamp()) are preferred over hard-coded pixels for font sizing in modern, accessible CSS: they respect the user’s browser zoom and font-size preferences.

Font weight and style are requested as numbers and keywords, but what actually renders depends on which weights and styles the chosen font file ships. A font family that only includes a 400 (regular) cut will have the browser synthesize a fake bold or italic by algorithmically slanting or thickening the glyphs when you ask for font-weight: 700 or font-style: italic — this synthetic version usually looks worse than a font’s real bold/italic cut, which is one reason variable fonts (a single file covering a full weight range) have become popular.

Syntax

The core font properties, used individually or combined via the font shorthand:

selector {
  font-family: value;
  font-size: value;
  font-weight: value;
  font-style: value;
  font-variant: value;
  line-height: value;
  font: value;
}
Property Common values Notes
font-family "Georgia", serif Comma-separated fallback list; generic family (serif, sans-serif, monospace, cursive, system-ui) should always be last.
font-size 16px, 1rem, clamp(1rem, 2vw, 1.5rem) Base for em/rem calculations; keyword values like small, large also exist.
font-weight 400, 700, bold, normal Numeric range is 1–1000; only weights the font file provides render natively.
font-style normal, italic, oblique oblique can take an angle, e.g. oblique 10deg.
line-height 1.5, 24px, 150% Unitless numbers are recommended — they scale relative to the element’s own font-size.
font shorthand Order: font-style font-variant font-weight font-size/line-height font-family; font-size and font-family are required.

The general shape of a rule looks like this — selector, property, and value below are placeholders, not real CSS:

selector {
  property: value;
}

Examples

Example 1: A solid font stack with fallbacks

body {
  font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 16px;
  line-height: 1.5;
  color: #222222;
}

h1 {
  font-family: Georgia, "Times New Roman", serif;
  font-weight: 700;
  font-size: 2.25rem;
}

Result: Body text renders in whichever of Segoe UI, Roboto, Helvetica Neue, or Arial is installed on the reader’s machine, at 16px with generous 1.5x line spacing. Headings switch to a serif stack (Georgia if present, otherwise Times New Roman, otherwise the browser’s default serif) in bold at 36px.

This is explained by the fallback chain: the browser tries each family left to right and uses the first match. Because the final entry is a generic family (sans-serif / serif), text is guaranteed to render in some reasonable font even on a system with none of the named fonts installed — that safety net is why you should never omit the generic keyword.

Example 2: Loading a custom web font with @font-face

@font-face {
  font-family: "Inter";
  src: url("/fonts/Inter-Regular.woff2") format("woff2"),
       url("/fonts/Inter-Regular.woff") format("woff");
  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;
}

p {
  font-family: "Inter", system-ui, sans-serif;
}

Result: Paragraph text initially shows the system-ui fallback font, then swaps to the downloaded Inter typeface as soon as the woff2 file finishes loading (thanks to font-display: swap), avoiding invisible text while the font downloads. Bold text within paragraphs automatically pulls the dedicated Bold file instead of a synthesized bold.

Each @font-face block registers one weight/style combination under the shared family name "Inter". When later CSS asks for font-weight: 700 on that family, the browser matches it to the second block’s real bold file rather than faking bold from the regular glyphs — this is why professional type systems register a @font-face block per weight/style pair actually used.

Example 3: Fluid font sizing with the font shorthand

:root {
  --heading-font: "Georgia", serif;
}

.card-title {
  font: italic 600 clamp(1.25rem, 1rem + 1.5vw, 2rem)/1.2 var(--heading-font);
  letter-spacing: 0.01em;
}

Result: The title renders italic, at a semi-bold weight of 600, in the Georgia/serif stack, with line-height set to 1.2x the resolved font size. Its font size scales smoothly between 1.25rem on narrow viewports and 2rem on wide ones as the viewport width changes, instead of jumping between fixed breakpoints.

This uses the font shorthand, which packs style, weight, size/line-height, and family into one declaration — note that font-size and font-family are the only two parts that are mandatory; anything omitted resets to its initial value, which is a common gotcha covered below. The clamp(min, preferred, max) function computes the preferred value from viewport width but never goes below the min or above the max.

How it works step by step

  • 1. Parse the family list: The browser reads font-family as an ordered, comma-separated list.
  • 2. Match against available fonts: For each candidate, it checks locally installed fonts and any fonts registered via @font-face with a matching font-family name.
  • 3. Match weight/style within the family: Once a family matches, the browser looks for a face registered with the requested font-weight/font-style. If an exact cut isn’t available, it picks the closest weight per the CSS font-matching algorithm, or synthesizes bold/italic if allowed.
  • 4. Resolve size: Relative units resolve against context — em against the element’s inherited font-size, rem against the root element’s font-size, and viewport units/clamp() against the viewport dimensions.
  • 5. Compute line box height: line-height (unitless numbers multiply the element’s own font-size) determines the height of each line box, which affects vertical spacing between wrapped lines.
  • 6. Shape and paint glyphs: The chosen font file’s glyph outlines are shaped (including ligatures/kerning) and painted using the resolved size, weight, and style.

Common Mistakes

Mistake 1: No fallback or generic family

p {
  font-family: Montserrat;
}

If Montserrat isn’t installed and no @font-face registers it, the browser silently falls back to its own default font with no guarantee of what that looks like across operating systems. Always end the stack with a generic family and, ideally, list common substitutes in between:

p {
  font-family: Montserrat, "Helvetica Neue", Arial, sans-serif;
}

Mistake 2: Forgetting quotes on multi-word names

Font family names containing spaces or that could be mistaken for multiple keywords should be quoted. This is invalid because Times New Roman is unquoted and contains an unexpected token:

h1 {
  font-family: Times New Roman serif;
}

Correct version, with the multi-word name quoted and a comma separating stack entries:

h1 {
  font-family: "Times New Roman", serif;
}

Mistake 3: Using the font shorthand and accidentally resetting values

Because font is a shorthand, any longhand you don’t include gets reset to its initial value — a later font declaration can silently wipe out a font-weight or line-height you set earlier:

.title {
  font-weight: 700;
  font: 1.5rem/1.4 Arial, sans-serif;
}

Here the second rule resets font-weight back to normal because the shorthand omitted it. Fix by including weight in the shorthand, or declaring the shorthand first and the override after:

.title {
  font: 700 1.5rem/1.4 Arial, sans-serif;
}

Best Practices

  • Always end a font-family stack with a generic family (sans-serif, serif, monospace, or system-ui) as a last-resort fallback.
  • Quote font names that contain spaces or special characters.
  • Prefer relative units (rem, em, %, clamp()) for font-size so text respects user zoom and browser font-size settings.
  • Register one @font-face block per weight/style you actually use, rather than relying on synthetic bold/italic.
  • Set font-display: swap (or another appropriate value) on @font-face rules to avoid invisible text while a web font downloads.
  • Use unitless line-height values so spacing scales correctly with each element’s own font size.
  • Limit the number of distinct font families and weights loaded per page — each one is a separate network request that delays rendering.

Practice Exercises

  • Exercise 1: Write a rule for body that sets a five-item font-family stack ending in a generic family, a font-size of 1rem, and a unitless line-height of 1.6.
  • Exercise 2: Register a custom font named "Roboto Slab" via @font-face for both a regular (400) and bold (700) weight, each pointing at a different .woff2 file, with font-display: swap.
  • Exercise 3: Using the font shorthand, write one rule for .hero-heading that is italic, weight 800, has a fluid size between 2rem and 4rem using clamp(), a line-height of 1.1, and a font-family of "Poppins", sans-serif. Expected result: bold italic text that grows and shrinks smoothly with the viewport while staying tightly spaced vertically.

Summary

  • font-family is an ordered fallback list; the browser uses the first family it can match, so always end with a generic family.
  • Font properties inherit by default, letting you set typography once near the top of the document and override only where needed.
  • font-size underlies em/rem calculations elsewhere in your CSS, so relative units keep the whole page scalable and accessible.
  • font-weight/font-style only render natively if the loaded font file includes that cut; otherwise the browser synthesizes an approximation.
  • @font-face lets you load custom fonts, matching one block per weight/style combination you actually use.
  • The font shorthand is convenient but resets any longhand you omit — include everything you need in one declaration.