Lesson 13 / 22
Line Length and Spacing
Computing line length in characters from column width, tying leading to line length, the return-sweep-angle criterion, and the lower bound on paragraph spacing.
Contents
The scale gave the size of the text. The summary paragraph on the record detail screen takes a number from the scale — 16 pixels, step zero — and spreads across the full column the layout gives it. On a wide screen, that column opens up to 880 pixels. Is this paragraph readable?
The question falls on the second side of the distinction defined in the previous lesson. The typeface decision is made, the size decision is made; both were on the legibility side. Readability, however, is measured at the block level and depends on two quantities: how long a line is and how far apart the lines sit from each other. These two quantities are not independent; when one changes, the other has to change too.
Line Length Is a Character Measure, Not a Pixel Measure
Line length is the number of characters that fit on a line. The reason it is measured in characters rather than pixels is that the distance the eye covers returning from the end of a line to the start of the next relates to word count; the same pixel width carries a different number of words at different type sizes.
Character count cannot be measured directly, because characters have different widths. A typeface does, however, declare each character’s advance width; it can be computed by averaging over real text.
// linelength.mjs — computing line length in characters from column width // Advance widths the candidate family declares (em = 1000 units). const G = { " ": 260, a: 556, b: 574, c: 500, d: 574, e: 545, f: 340, g: 574, h: 560, i: 244, j: 244, k: 520, l: 244, m: 856, n: 560, o: 570, p: 574, q: 574, r: 366, s: 480, t: 358, u: 560, v: 508, w: 800, x: 500, y: 508, z: 470, A: 660, B: 660, C: 686, D: 720, E: 610, F: 610, G: 740, H: 730, I: 280, J: 280, K: 660, L: 610, M: 890, N: 730, O: 760, P: 610, R: 660, S: 620, T: 620, U: 720, V: 660, W: 890, X: 660, Y: 620, Z: 600, ",": 260, ".": 260, ":": 260, "-": 340, "1": 560, "9": 560, "0": 560, "8": 560, }; const DEFAULT = 560; const summary = "This volume traces the development of optics together with the changing " + "instruments of observation. The author first frames light and shadow as a " + "problem of measurement, then shows how theories of vision follow from these " + "measurements. The bibliography is extensive, the index detailed, and the " + "plates are taken from original prints."; const total = [...summary].reduce((s, c) => s + (G[c] ?? DEFAULT), 0); const average = total / [...summary].length; const SIZE = 16; // px const averagePx = (average / 1000) * SIZE; console.log(`summary text character count: ${[...summary].length}`); console.log(`average advance width: ${average.toFixed(1)} / 1000 em`); console.log(`average character width at 16 px: ${averagePx.toFixed(3)} px\n`); const TARGET = [45, 75]; console.log("column (px) line length (characters) target 45-75"); for (const column of [320, 480, 560, 640, 720, 880, 1040]) { const characters = column / averagePx; const status = characters < TARGET[0] ? "SHORT" : characters > TARGET[1] ? "LONG" : "fits"; console.log(`${String(column).padStart(11)} ${characters.toFixed(1).padStart(26)} ${status}`); } console.log("\ntarget character count -> required column width"); for (const k of [45, 66, 75]) { console.log(`${String(k).padStart(3)} characters -> ${(k * averagePx).toFixed(1)} px`); } // If size changes at a fixed column width, line length changes too. console.log("\ncolumn fixed at 640 px -> size line length (characters)"); for (const b of [13, 16, 20, 25]) { const width = (average / 1000) * b; console.log(`${String(b).padStart(24)} px ${(640 / width).toFixed(1).padStart(24)}`); }
summary text character count: 333
average advance width: 455.9 / 1000 em
average character width at 16 px: 7.295 px
column (px) line length (characters) target 45-75
320 43.9 SHORT
480 65.8 fits
560 76.8 LONG
640 87.7 LONG
720 98.7 LONG
880 120.6 LONG
1040 142.6 LONG
target character count -> required column width
45 characters -> 328.3 px
66 characters -> 481.4 px
75 characters -> 547.1 px
column fixed at 640 px -> size line length (characters)
13 px 108.0
16 px 87.7
20 px 70.2
25 px 56.2
The 45–75 character range is the criterion this course adopts and quantifies so it can be audited; the lower bound keeps lines from breaking too often, the upper bound keeps the return sweep from becoming difficult.
The output’s second block is the real finding. An 880-pixel column fits 120.6 characters, roughly 1.6 times the upper bound. When the summary paragraph spreads across the full column, it misses the readability criterion not at a single point but across the entire span of a wide screen. Every column width from 560 pixels up falls outside the criterion.
The third block gives the measure of the solution. Whatever the column’s pixel width, the summary text’s width should be at most 547 pixels. This is a constraint independent of the layout’s column width: the text block carries its own maximum width, and if the column is wider than that, the text does not fill the column.
The fourth block exposes a trap. Holding the column fixed and changing the size still changes line length: in a 640-pixel column, 108.0 characters at 13 pixels, 56.2 at 25 pixels. Although line length looks like a layout decision, it is a direct consequence of the step chosen from the scale. A heading’s line length and body text’s line length differ even in the same column, and the upper bound is audited separately for each level.
The constraint’s written form is a maximum width given to the text block, not the column:
.summary { font-size: 16px; max-width: 547px; }
This value also has a unit that can be written in characters: the ch unit uses the
advance width of the digit zero in the current typeface. Because zero’s width is not
equal to the average character width, a number given in ch does not equal the
computed character count; this lesson computes the measure through the average and
writes the result in pixels.
Leading Depends on Line Length
Leading is the distance between the baselines of two consecutive lines. In the
Visual Presentation with CSS course, this measure was given through the line-height
property and was called line height there; written unitless, it works as a
multiplier of the font size. On the design side, the question asked is what that
multiplier should be.
The multiplier has two separate lower bounds, and both are computable.
The first lower bound comes from the typeface. The sum of ascent and descent computed in the previous lesson is the vertical measure the letters actually occupy. When the multiplier drops below this value, the descenders of one line overlap the ascenders of the next.
The second lower bound comes from eye movement. At the end of a line, the eye makes a return sweep to the start of the next line. This movement’s horizontal component is line length, its vertical component is leading. Their ratio gives the angle at which the sweep happens; as the angle shrinks, it becomes harder for the target line to stand apart from its neighbors, and the eye reads the same line twice or skips a line.
// leading.mjs — line box, inter-line space, return-sweep angle, and paragraph spacing const SIZE = 16; // px const ASCENT_DESCENT = 1.051; // candidate-3's declared ascent+descent (em) const ANGLE_THRESHOLD = 2.0; // smallest return-sweep angle this course adopts (degrees) const degrees = (rad) => (rad * 180) / Math.PI; console.log("multiplier line box inter-line space paragraph spacing >="); for (const m of [1.2, 1.3, 1.4, 1.5, 1.6, 1.75]) { const box = m * SIZE; const space = box - ASCENT_DESCENT * SIZE; console.log( `${m.toFixed(2).padStart(10)} ${box.toFixed(2).padStart(14)} px ${space.toFixed(2).padStart(17)} px ${(space * 2).toFixed(2).padStart(17)} px` ); } console.log("\nline length (px) -> return-sweep angle (degrees), by multiplier"); const multipliers = [1.2, 1.4, 1.5, 1.6, 1.75]; console.log("length " + multipliers.map((m) => m.toFixed(2).padStart(7)).join("")); for (const length of [328, 481, 547, 640, 880]) { const row = multipliers .map((m) => degrees(Math.atan((m * SIZE) / length)).toFixed(2).padStart(7)) .join(""); console.log(`${String(length).padStart(7)} ${row}`); } console.log("\nline length -> smallest multiplier meeting the threshold (2.0 degrees)"); for (const length of [328, 481, 547, 640, 880]) { const fromAngle = (length * Math.tan((ANGLE_THRESHOLD * Math.PI) / 180)) / SIZE; // The multiplier cannot go below the ascent+descent value, or lines start to overlap. const lower = Math.max(fromAngle, ASCENT_DESCENT); const chosen = Math.ceil(lower * 20) / 20; // round to 0.05 steps console.log( `${String(length).padStart(4)} px -> from angle ${fromAngle.toFixed(3)} lower bound ${lower.toFixed(3)} chosen ${chosen.toFixed(2)}` ); }
multiplier line box inter-line space paragraph spacing >=
1.20 19.20 px 2.38 px 4.77 px
1.30 20.80 px 3.98 px 7.97 px
1.40 22.40 px 5.58 px 11.17 px
1.50 24.00 px 7.18 px 14.37 px
1.60 25.60 px 8.78 px 17.57 px
1.75 28.00 px 11.18 px 22.37 px
line length (px) -> return-sweep angle (degrees), by multiplier
length 1.20 1.40 1.50 1.60 1.75
328 3.35 3.91 4.18 4.46 4.88
481 2.29 2.67 2.86 3.05 3.33
547 2.01 2.34 2.51 2.68 2.93
640 1.72 2.00 2.15 2.29 2.51
880 1.25 1.46 1.56 1.67 1.82
line length -> smallest multiplier meeting the threshold (2.0 degrees)
328 px -> from angle 0.716 lower bound 1.051 chosen 1.10
481 px -> from angle 1.050 lower bound 1.051 chosen 1.10
547 px -> from angle 1.194 lower bound 1.194 chosen 1.20
640 px -> from angle 1.397 lower bound 1.397 chosen 1.40
880 px -> from angle 1.921 lower bound 1.921 chosen 1.95
The two-degree threshold is likewise a criterion this course adopts; what matters is not the number itself but that the decision is tied to a number.
The last block shows how the two lower bounds trade places. At 328 and 481 pixels, the angle criterion is met by multipliers of 0.716 and 1.050; both sit below the 1.051 bound the typeface imposes, so the typeface is decisive. From 547 pixels on, the angle criterion takes over, demanding a multiplier of 1.921 at 880 pixels.
This confirms the previous section’s finding by a second route. An 880-pixel line looks “rescuable” by raising the leading multiplier to 1.95. But at 16-pixel text, a 1.95 multiplier means a 31.2-pixel line box; the gap between lines approaches the letters’ own height, and the block stops being a set of lines and turns into a series of separate lines. The real fix for a long line is not to increase the leading, it is to shorten the line.
The second table carries a warning in the opposite direction. At the short, 328-pixel line, a 1.75 multiplier produces a return-sweep angle of 4.88 degrees. A large angle is not itself a problem; but the inter-line space climbing to 11.18 pixels causes the lines in a short-line block to come apart from each other. A short line wants little space, a long line wants a lot of space; these are the two ends of a single rule.
Paragraph Spacing Derives From Inter-Line Space
The last column of the first table gives a side effect of leading. Where one paragraph ends and the next begins is legible only if the space between paragraphs is distinctly larger than the inter-line space. Taken as double for a criterion, text set at a 1.5 multiplier needs a paragraph spacing of at least 14.37 pixels.
This is the typographic application of the relative-spacing principle established in the Proximity and Grouping lesson: the ratio between within-group spacing and between-group spacing determines the grouping. Here the group is the paragraph, and the within-group spacing is the inter-line space.
The same rationale explains why paragraph indentation and paragraph spacing are not used together. Both do the same job — announcing the paragraph boundary — and using both at once marks the boundary twice. The two-channel rule does not apply here, because when the paragraph boundary is lost, the cost the reader pays is not a loss of hierarchy but merely a pause.
Alignment and Breaking
Once line length is fixed, what remains is how line endings get broken.
In left-aligned text, the right edge is ragged and word spacing stays fixed. In justified text, the right edge straightens, but word spacing stretches line by line. In narrow columns this stretching grows and produces visibly empty channels within the line. Because the catalog interface’s summary paragraph can drop close to 45 characters, it is set left-aligned.
Centered text is unsuited to continuous reading, because every line starts at a different point and the return sweep’s target changes line by line. Centering can be used for single-line or at most two-line text — an empty-state message, button text — and these are treated in the Component States topic.
Word breaking reduces stretching in justified typesetting; in left-aligned typesetting, it straightens the right edge but breaks word integrity. In the catalog interface, author names and work titles are proper names and must not be broken; breaking is therefore enabled only in the summary paragraph and stays off in the title and metadata fields.
Summary
- Line length is measured in characters; even if the pixel width stays the same, the character count changes when the type size changes.
- Character count is computed by averaging the typeface’s declared advance widths over real text, and it is turned into a maximum-width constraint.
- The long-line problem is solved by a width limit given to the text block, not the column; the limit is computed separately for each text level.
- The leading multiplier has two lower bounds: the typeface’s ascent-plus-descent sum and the return-sweep-angle criterion. The first is decisive on short lines, the second on long lines.
- Increasing the spacing does not rescue a long line; as the required multiplier grows, the block stops being a set of lines.
- Paragraph spacing must be distinctly larger than inter-line space; the paragraph boundary is announced by indentation or spacing, not both.
Next Step
These three lessons fixed the form of the text: which family, which size, which width, and which spacing. The color of text and ground, however, is still nothing more than the dark gray and white from the first lesson. The interface’s need for color is not a decorative need: separating the primary action from the secondary action, announcing that a record is selected, and showing shelf-availability status all require separate surfaces. The next lesson defines colors not one by one but as roles, sets the roles onto a scale along the lightness axis, and determines by computation which ground each role will be matched with.
To keep your progress and take notes, Log in
My notes
Log in to take notes.