Skip to content
academia.sh

Lesson 19 / 26

Typography

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

Contents

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.

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.

// 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:

bn=b0rnb_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:

: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.

/* 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close