AniUI Academy

Typography

Font stacks and the five generic fallback families, loading a web font, font-weight/style/line-height, text-align/letter-spacing/text-transform, and the zero-download system font stack.

9 min read

Every piece of text on a page runs through the same handful of properties, and getting them right is most of the difference between a page that reads comfortably and one that's technically legible but tiring to actually read.

Font stacks and the five generic families

You rarely specify just one font — you specify a stack, a comma-separated list the browser tries in order, falling through to the next if one isn't available on the visitor's device:

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

Read that as: "use Helvetica Neue if it's installed; if not, try Arial; if neither is available, fall back to some sans-serif font." Font names with spaces need quotes ("Helvetica Neue"); single-word names don't need them, though quoting everything consistently is a fine habit too.

The last entry in a font stack should almost always be one of CSS's five generic font families, because every browser ships a default font for each one — they are the one guaranteed resolution point in a font stack:

  • serif — fonts with small decorative strokes at the ends of letters (Times New Roman-like).
  • sans-serif — fonts without those strokes, cleaner and more geometric (Arial/Helvetica-like). The most common choice for body UI text.
  • monospace — every character occupies the same width, used for code.
  • cursive — script-like, handwriting-style fonts.
  • fantasy — decorative, stylized display fonts.

Ending a stack with a specific font and no generic fallback is a real gap: if that one specific font somehow isn't available, the browser falls back to its own arbitrary default rather than something you chose.

Loading a web font

If none of the fonts already on a visitor's device are right, you can load one over the network. There are two common ways.

A <link> to a font hosting service, added in <head>:

<link rel="stylesheet" href="https://example-font-service.com/fonts.css" />

The service serves a small CSS file that itself declares the font, and your page then references it by name in font-family like any other font. (Exact URLs and setup steps differ per service — check the specific provider you're using for the real syntax.)

@font-face, which you write yourself, pointing directly at a font file:

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
 
body {
  font-family: "Inter", sans-serif;
}

@font-face gives the font you're loading a name ("Inter" here — it can be anything, it doesn't have to match the file), points src at the actual font file, and once declared, that name is usable anywhere font-family is used, same as a font already installed on the device. font-display: swap tells the browser to show a fallback font immediately and swap in the web font once it's downloaded, instead of leaving text invisible while the font loads.

font-weight and font-style

.bold-text   { font-weight: 700; }   /* or the keyword bold */
.normal-text { font-weight: 400; }   /* or the keyword normal */
.italic-text { font-style: italic; }

font-weight accepts numeric values in steps of 100 from 100 (thinnest) to 900 (heaviest), with 400 as normal weight and 700 as bold — though whether a given font actually ships all nine weights depends on the font itself; many only include a few. font-style: italic slants the text; normal (the default) doesn't.

line-height

line-height controls the vertical space a line of text occupies — how far apart stacked lines sit from each other.

p {
  line-height: 1.5;
}

Write it unitless — a plain number like 1.5, not 1.5em or 24px — because a unitless value scales relative to that element's own font-size and is inherited as that ratio, so a heading and a paragraph with different font sizes each get proportionally appropriate spacing from one shared rule, rather than every descendant inheriting one fixed pixel value regardless of its own size.

The well-established readability guideline for body text is roughly 1.4 to 1.6. Much tighter than that (close to 1) and lines crowd together, making it easy to lose your place moving from the end of one line to the start of the next. Much looser and paragraphs feel disconnected, harder to read as a continuous block. Headings can often go tighter than body text — 1.1 to 1.3 — since large type has more inherent visual separation between lines already.

text-align, letter-spacing, text-transform

.centered  { text-align: center; }
.wide-caps {
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
  • text-alignleft, right, center, justify (stretches each line to fill the full width, adjusting word spacing — used carefully, since it can produce uneven, rivers-of-space gaps in narrow columns).
  • letter-spacing — extra space between characters, positive to spread them out, negative to tighten them. Small values (0.02em0.08em) paired with uppercase is a common combination for labels and eyebrow text, since all-caps text otherwise tends to look visually cramped.
  • text-transform — changes the displayed case without touching the underlying text content: uppercase, lowercase, capitalize (capitalizes the first letter of each word). Useful because it lets you write normal sentence-case content in your HTML while still displaying it differently.

The system font stack

body {
  font-family: system-ui, sans-serif;
}

system-ui is a special keyword that resolves to whatever font the visitor's own operating system uses for its native interface — San Francisco on macOS and iOS, Segoe UI on Windows, Roboto on Android, and so on. Nothing is downloaded, because the font is already present on the device; the stylesheet just asks for it by role rather than by name.

This is a real, common, zero-cost approach used across a huge share of production sites and native-feeling web apps: it costs no network request, never causes a flash of invisible or swapped-out text while a web font loads, and it makes your interface feel visually native to whatever platform it's running on, since it matches the same font the rest of that OS's interface uses.

Try it yourself

Try it yourself
Loading playground...

What to remember

  • A font stack tries each entry in order and should always end in a generic family (sans-serif, serif, monospace, cursive, fantasy) as the guaranteed fallback.
  • Load a web font via a font service's <link>, or with @font-face pointing src at a font file — either makes the font usable in font-family like any installed one.
  • line-height should be unitless; roughly 1.4-1.6 is the well-established readability range for body text.
  • text-align, letter-spacing and text-transform control alignment, character spacing, and displayed case without changing the underlying text.
  • font-family: system-ui, sans-serif; uses the visitor's OS default interface font with zero download and no flash of unstyled text.

Check yourself

3 questions · pass 3/3 to unlock CSS Custom Properties (Variables)

up to 50
  1. 1.In font-family: "Helvetica Neue", Arial, sans-serif;, what does the final sans-serif entry do?

  2. 2.What line-height range is the well-established readability guideline for body text?

  3. 3.What does font-family: system-ui, sans-serif; actually load?

3 left to answer