Styling text: fonts and line height
Almost everything on a web page is text. Before you touch layout, getting type right buys you more than any other set of declarations — and two of the properties involved have defaults that are actively wrong for reading.
font-family is a list of hopes
You do not install a font on someone's computer by naming it. You give the browser a list, and it works left to right until it finds one that exists.
body {
font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
A Windows machine stops at "Segoe UI". An Android phone falls through to
Roboto. Something unusual falls all the way to sans-serif, which is not a
font at all but a generic family — an instruction meaning "whatever this
system considers its default sans-serif".
That last entry is not optional decoration. Without it, a device with none of your named fonts falls back to the browser default, which is usually a serif, and your carefully sans-serif design renders in Times New Roman.
The five generics worth knowing: sans-serif, serif, monospace, cursive,
system-ui. system-ui is a good default in itself — it resolves to whatever
the operating system uses for its own interface, so the page feels native and
loads instantly, because the font is already there.
Two syntax details:
- Quote names containing spaces.
"Segoe UI"needs quotes;Robotodoes not. Quoting single-word names anyway is harmless. - Never quote a generic.
"sans-serif"in quotes is read as a request for a font literally named "sans-serif", which does not exist, and the fallback silently does nothing.
There is one trap specific to monospace. Write font-family: monospace on
its own and most browsers shrink the text to about 13px, an old default that
survives for compatibility reasons. The workaround is to name the generic twice
over, or to set a size explicitly:
code {
font-family: ui-monospace, "Cascadia Code", Consolas, monospace, monospace;
}
The doubled monospace looks like a typo and is a genuine, widely used fix.
line-height, and why the unit matters
The default line-height is around 1.2 — cramped for anything longer than a heading. Body text reads better at 1.5 to 1.6.
body {
line-height: 1.5;
}
No unit. That is deliberate, and it is one of the few places in CSS where the absence of a unit changes the behaviour rather than just the number.
A unitless line-height inherits the ratio. A line-height with a unit inherits the computed result.
Consider a page with a large pull quote inside it:
body { font-size: 16px; line-height: 1.5; }
.quote { font-size: 32px; }
The quote inherits the ratio 1.5, works it out against its own 32px, and gets 48px of leading. Proportional, correct, no extra rule needed.
Now the same thing with a unit:
body { font-size: 16px; line-height: 24px; } /* or 150% — same problem */
.quote { font-size: 32px; }
24px was computed once, on body, and 24px is what every descendant
inherits. The quote gets 32px letters crammed into 24px lines, and the
descenders collide with the line below. 150% behaves identically, because a
percentage is also resolved at the point it is declared.
So: unitless for line-height, always, unless you have a specific reason to freeze the value.
Headings are the exception to the number itself, not the unit. Large text needs proportionally less leading, so tighten it:
h1, h2, h3 { line-height: 1.2; }
p { line-height: 1.6; }
font-weight is numbers, not names
bold is a name for 700. normal is 400. The numeric scale runs 100 to 900 in
hundreds:
| Value | Common name |
|---|---|
| 300 | Light |
| 400 | Regular / normal |
| 500 | Medium |
| 600 | Semibold |
| 700 | Bold |
The catch: a weight only exists if the font file provides it. Ask for 600 from a family shipping only 400 and 700, and the browser does one of two things — rounds to a real weight, or synthesises one by algorithmically thickening the letterforms. Synthetic bold looks smeared, and it is the reason a heading sometimes appears slightly wrong in a way you cannot name.
If you load a web font, load the weights you actually use, and use the weights you actually loaded.
font-size, and the one you must not set
Set sizes in rem so they scale with the reader's browser setting:
h1 { font-size: 2rem; } /* 32px at a 16px root */
h2 { font-size: 1.5rem; }
p { font-size: 1rem; }
small { font-size: 0.875rem; }
Do not set font-size on html. That is the root the reader controls, and
overriding it overrides an accessibility preference.
For headings that should scale with the viewport, clamp() is the tidy way:
h1 {
font-size: clamp(1.75rem, 5vw, 3rem);
}
Small screens get 28px, large screens 48px, and in between it tracks the window. No media queries.
letter-spacing, in the two directions it helps
Default spacing is right for body text at body sizes. It stops being right at the extremes.
Large headings are set too loose by default, because type designers space letters for reading sizes. Pull them in slightly:
h1 {
font-size: 3rem;
letter-spacing: -0.02em;
}
Uppercase text is set too tight, because capitals were spaced assuming lower-case neighbours. Open it up:
.label {
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.08em;
}
Note em in both, not px — the spacing then tracks the font size, which is
exactly what you want here. This is the case the units
lesson meant by "spacing that belongs to the type".
Never letter-space body text. Even a small positive value measurably slows reading.
text-transform changes appearance, not content
.label { text-transform: uppercase; }
The HTML still says Book seat. Only the rendering shouts. That matters,
because a screen reader reads the underlying text — and because you can change
your mind later without editing markup.
capitalize is the one to be careful with. It upper-cases the first letter of
every word, including the ones that should stay lower-case: "Karachi To
Hyderabad", "Book A Seat". For titles, write the capitals you want in the HTML.
Putting it together
A defensible set of type defaults for a content site:
body {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 1rem;
line-height: 1.6;
color: #1a1a1a;
}
h1, h2, h3 {
line-height: 1.2;
letter-spacing: -0.01em;
font-weight: 700;
}
h1 { font-size: clamp(1.75rem, 5vw, 3rem); }
h2 { font-size: 1.5rem; }
p {
max-width: 65ch;
margin-block: 0 1em;
}
code {
font-family: ui-monospace, Consolas, monospace, monospace;
font-size: 0.9em;
}
Nine declarations of substance, and a page that reads properly on a phone and on a desktop, at whatever size the reader has chosen.
That closes the colour, text and units section. If a rule here loads but does nothing, the cause is almost always the selector — combinators — or a discarded value, which the calc lesson shows you how to spot in devtools.
Prefer learning with a teacher?
WasiLearn Academy runs small-group classes covering this material, online and in Karachi.
See the classes