---
title: Typography
source: 'https://academia.sh/en/courses/css-fundamentals/typography'
course: 'Visual Presentation with CSS'
language: en
updated: '2026-08-17T18:09:20+00:00'
license: 'CC BY-SA 4.0'
---

# Typography

Resolving a font family list, weight and style axes, a modular size scale, and the calculable rationale for writing line height unitless.

The color decisions are made, but the text itself has not been
touched. The measurement station page has a heading, a masthead
paragraph, a measurement table, and an explanatory note; all of them
stand in the same font, the same size, and the same line spacing.

This lesson focuses on typography's three axes: which font, what
size, what line spacing. All three have a calculable side, and the
decisions get made not one by one, but as a **scale**.

## A Font Family Is a List

`font-family` does not take a name, it takes a comma-separated
**priority list**. The list gets tried left to right, and the first
family found gets used.

```css
body { font-family: "Inter", system-ui, sans-serif; }
```

Resolution happens at the character level: if the first family on the
list does not contain a character, the next family gets checked for
that character. Two characters on the same line in a document can
come from two different families. In accented text, this can lead to
accented letters getting taken from a different family on the list,
and the line looking inconsistent.

There is always a **generic family** at the end of the list: `serif`,
`sans-serif`, `monospace`, `cursive`, `system-ui`, `ui-monospace`.
These name a class, not a specific font; the environment decides
which file gets used. If a generic family does not get written and no
name on the list gets found, the browser's default font gets used —
this means letting go of control entirely.

If a name contains a space, it gets put in quotes. Generic families
are keywords and do not get quoted; if quoted, they get searched for
like a family name and do not get found.

## Weight and Style Are Separate Axes

`font-weight` takes a number between 100 and 900, or the keywords
`normal` (400) and `bold` (700). If a family has no file matching the
declared weight, the implementation picks the closest one; it is also
possible for it to synthesize a weight that is not found, and the
result looks different from a genuine bold cut.

`font-style` takes three values: `normal`, `italic`, `oblique`. There
is a distinction between the first two — `italic` asks for the
family's separately drawn slanted cut, `oblique` accepts a slanted
version of the upright cut. If the family has no slanted cut,
`italic` also gets produced by slanting.

These two axes' relationship to the document needs establishing
separately. The Web Fundamentals and HTML curriculum established that
the `strong` and `em` elements declare meaning, while `b` and `i`
declare only appearance. Writing `strong` to show a text as bold
loads that text with the meaning "important." If only a visual
emphasis is wanted, the decision gets made in the style sheet.

## Line Height Gets Written Unitless

`line-height` can take a number, a length, or a percentage. All three
give the same first result, but their **inheritance** behavior
diverges.

```js
// typography.mjs — the unitless-vs-unit line-height difference; modular scale
console.log("--- line-height: a unitless value gets inherited as a ratio ---");
const tree = [
  { name: "body",  fontSize: 16 },
  { name: "main",  fontSize: 16 },
  { name: "h1",    fontSize: 32 },
  { name: "small", fontSize: 12 },
];

for (const format of ["1.5", "24px", "150%"]) {
  console.log(`\nbody { line-height: ${format} }`);
  const root = tree[0];
  for (const node of tree) {
    let lineHeight;
    if (format.endsWith("px"))      lineHeight = parseFloat(format);            // computed: fixed px
    else if (format.endsWith("%"))  lineHeight = root.fontSize * parseFloat(format) / 100; // resolved on body
    else                            lineHeight = node.fontSize * parseFloat(format);  // recomputed on every element
    console.log(`  ${node.name.padEnd(6)} font-size=${String(node.fontSize).padStart(2)}px -> line-height=${lineHeight}px  (ratio ${(lineHeight / node.fontSize).toFixed(2)})`);
  }
}

console.log("\n--- modular scale: base 16px, ratio 1.25 ---");
const base = 16, ratio = 1.25;
const steps = [-2, -1, 0, 1, 2, 3, 4];
for (const n of steps) {
  const px = base * Math.pow(ratio, n);
  console.log(`step ${String(n).padStart(2)}: ${px.toFixed(2)}px = ${(px / base).toFixed(4)}rem`);
}

console.log("\n--- line length ---");
const AVG_CHAR = 0.5; // average character width, in em (example value)
for (const fontSize of [16, 18, 20]) {
  for (const width of [480, 640, 800]) {
    const chars = width / (fontSize * AVG_CHAR);
    const fits = chars >= 45 && chars <= 75 ? "fits" : "does not fit";
    console.log(`font-size=${fontSize}px, box=${width}px -> ~${chars.toFixed(0)} chars/line  ${fits}`);
  }
}
```

```
--- line-height: a unitless value gets inherited as a ratio ---

body { line-height: 1.5 }
  body   font-size=16px -> line-height=24px  (ratio 1.50)
  main   font-size=16px -> line-height=24px  (ratio 1.50)
  h1     font-size=32px -> line-height=48px  (ratio 1.50)
  small  font-size=12px -> line-height=18px  (ratio 1.50)

body { line-height: 24px }
  body   font-size=16px -> line-height=24px  (ratio 1.50)
  main   font-size=16px -> line-height=24px  (ratio 1.50)
  h1     font-size=32px -> line-height=24px  (ratio 0.75)
  small  font-size=12px -> line-height=24px  (ratio 2.00)

body { line-height: 150% }
  body   font-size=16px -> line-height=24px  (ratio 1.50)
  main   font-size=16px -> line-height=24px  (ratio 1.50)
  h1     font-size=32px -> line-height=24px  (ratio 0.75)
  small  font-size=12px -> line-height=24px  (ratio 2.00)

--- modular scale: base 16px, ratio 1.25 ---
step -2: 10.24px = 0.6400rem
step -1: 12.80px = 0.8000rem
step  0: 16.00px = 1.0000rem
step  1: 20.00px = 1.2500rem
step  2: 25.00px = 1.5625rem
step  3: 31.25px = 1.9531rem
step  4: 39.06px = 2.4414rem

--- line length ---
font-size=16px, box=480px -> ~60 chars/line  fits
font-size=16px, box=640px -> ~80 chars/line  does not fit
font-size=16px, box=800px -> ~100 chars/line  does not fit
font-size=18px, box=480px -> ~53 chars/line  fits
font-size=18px, box=640px -> ~71 chars/line  fits
font-size=18px, box=800px -> ~89 chars/line  does not fit
font-size=20px, box=480px -> ~48 chars/line  fits
font-size=20px, box=640px -> ~64 chars/line  fits
font-size=20px, box=800px -> ~80 chars/line  does not fit
```

Comparing the three blocks gives the rule.

When the unitless `1.5` gets written, **the ratio gets inherited**:
every element multiplies its own font size by 1.5. The 32-pixel
heading got a line height of 48 units, the 12-pixel small text 18;
the ratio stayed 1.50 everywhere.

When `24px` or `150%` gets written, though, **the computed value gets
inherited**. The rule established in the Inheritance and Default
Values lesson gives its result here: `150%` gets resolved on the
`body` element and fixed at 24 pixels; child elements inherit that
fixed value. The 32-pixel heading's line height came out to 24 units
— a ratio of 0.75, meaning the line is **shorter** than the text. The
heading's lines overlap.

The applicable rule is this: `line-height` gets written unitless. The
unit writing gets used only on a single element, in a case where
inheritance is not wanted.

## Modular Scale

Font sizes do not get chosen one by one; they get derived from a base
and a ratio. **A modular scale** is a sequence produced by multiplying
consecutive sizes by a fixed ratio:

$$b_n = b_0 \cdot r^n$$

The output's second block produces seven steps with a 16-pixel base
and a 1.25 ratio. The results also get given converted to `rem`; this
directly shows how the scale gets written into the style sheet:

```css
:root {
  --scale-1: 0.64rem;
  --scale-2: 0.8rem;
  --scale-3: 1rem;
  --scale-4: 1.25rem;
  --scale-5: 1.5625rem;
  --scale-6: 1.9531rem;
  --scale-7: 2.4414rem;
}
```

The scale's value lies in making consistency mandatory: a heading
cannot get "a little bigger" a size; the next step on the scale gets
chosen. As the ratio grows, the difference between steps widens and
the hierarchy becomes clearer; as it shrinks, the scale compresses and
the levels move closer together.

## Line Length

The output's third block calculates the relationship between size and
box width. The established criterion for a readable line length is
45–75 characters per line; the calculation gets evaluated against this
range.

The output shows a trade-off. In a 640-unit box, 16-pixel text gives
about 80 characters per line — outside the range. In the same box,
18-pixel text gives 71 characters and falls inside the range.
Enlarging the font size **shortens** the line, because fewer
characters fit into the same width.

This is the rationale behind the `max-width: 70ch` declaration written
in the Box Sizing lesson: because the `ch` unit depends on character
width, when the font size changes the box width changes along with
it, and the line length stays inside the range. The 0.5 em average
character width in the calculation is an example value; the real
ratio varies by font.

```css
/* station.css — step 18: typography scale */
:root {
  --font-body: 1rem;
  --font-small: 0.8rem;
  --font-h3: 1.25rem;
  --font-h2: 1.5625rem;
  --font-h1: 2.4414rem;
}

body {
  font-family: system-ui, sans-serif;
  font-size: var(--font-body);
  line-height: 1.5;
}

h1 { font-size: var(--font-h1); line-height: 1.15; }
h2 { font-size: var(--font-h2); line-height: 1.2; }
h3 { font-size: var(--font-h3); line-height: 1.3; }

.source-note { font-size: var(--font-small); }

.measurement-table td.value {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}
```

Giving headings a **smaller** line-height ratio than the body text is
a typography rule: large-point lines are already far apart, and if
the same ratio gets kept, the spacing looks needlessly loose.

The last rule is specific to the measurement table. `tabular-nums`
asks for digits to get drawn at equal widths, so that the digits of
numbers stacked on top of each other line up. If the font does not
carry this feature, the declaration stays without effect and the
digits stay proportionally spaced — a situation that makes a numeric
table harder to read.

## Summary

- `font-family` is a priority list resolved at the character level;
  if the list has no generic family at the end, control gets left
  entirely to the environment.
- `font-weight` and `font-style` are separate axes; a weight or slant
  with no counterpart in the family gets synthesized by calculation
  and looks different from a genuine cut.
- When `line-height` gets written unitless, the ratio gets inherited
  and every element multiplies by its own size; in a unit or
  percentage writing, the computed value gets inherited, and lines
  overlap on large-point elements.
- Font sizes do not get chosen one by one; they get derived as a
  modular scale from a base and a ratio, and a hierarchy level
  corresponds to a step on the scale.
- Line length gets kept in the 45–75 character range; enlarging the
  font size shortens the line in the same box, so size and box width
  get decided together.

## Next Step

This lesson called the font by a name but did not ask where the file
behind that name comes from. How does a font get linked to the
document, what happens to the text while the file downloads, and how
does that wait affect the page's reading? The next lesson takes up
font loading and the display behavior during loading.
