WasiLearn Code
CSS · 2. Colour, text and units

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; Roboto does 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