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:
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-familyis 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-weightandfont-styleare 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-heightgets 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.