Line-Height and Rhythm
Chapter 02 ended on a loose thread: the measure you set depends on the leading. Here’s the other half of that decision.
Line-height is the CSS property that controls the vertical space a line of text occupies. Set it too tight and the lines crowd together. The eye finishes a line, sweeps back to the left margin, and has trouble locating the start of the next one because the lines above and below are visually close enough to compete for attention. The return sweep that Chapter 02 described as a horizontal problem is also a vertical one. Measure handles how far the eye travels left. Line-height handles how cleanly it lands on the right row.
Get both right and a paragraph reads without friction. Get either wrong and the reader feels it without being able to name it.
Unitless values, and why nothing else is acceptable
Line-height takes two kinds of values, and the difference between them is the single most important thing in this chapter.
A unitless value is a multiplier. line-height: 1.5 means “one and a half times the element’s own font size.” It’s computed per element, against whatever font size that element actually has.
A unit-based value is a fixed length. line-height: 24px means 24 pixels, full stop, regardless of font size.
The problem appears the moment a value is inherited. Line-height inherits like most CSS properties do. With a unitless value, children inherit the multiplier. With a unit-based value, children inherit the computed length.
Picture a container that sets line-height: 24px, sensible enough next to its 16px body text. Now nest a heading inside it:
.container {
font-size: 16px;
line-height: 24px; /* unit-based — the trap */
}
.container h2 {
font-size: 40px;
/* inherits line-height: 24px */
}The h2 is 40px tall but its line-height is still 24px, because that’s the computed value it inherited. The leading is now smaller than the text. Lines of the heading overlap. Nothing in the heading’s own rules looks wrong, which is what makes the bug hard to find.
The unitless version has no such failure:
.container {
font-size: 16px;
line-height: 1.5; /* unitless — the multiplier inherits */
}
.container h2 {
font-size: 40px;
/* inherits line-height: 1.5 → computes to 60px */
}The h2 inherits 1.5 and resolves it against its own 40px, giving 60px of line-height. Each child computes a leading that fits the size it actually is.
The rule is unconditional: always use unitless line-height. There is no situation on a normal web page where a unit-based value is the better choice, and plenty where it quietly breaks something three components away.
Optimal ranges
Unitless solves the inheritance problem. It does not pick the number for you. The right multiplier depends on what the text is doing.
Body text wants 1.5 to 1.65. Sustained reading needs room. The eye is making the return sweep hundreds of times down a page, and generous leading keeps each landing reliable. This is the range Chapter 02’s measure was tuned against.
Lead or intro text, the slightly larger paragraph that opens an article, sits a little tighter at 1.4 to 1.5. The larger font size already carries more visual weight, so it needs proportionally less added space.
Headings want 1.1 to 1.25. Large type has optical breathing room built into the glyphs themselves. A 40px heading at line-height: 1.5 reads as double-spaced and disconnected, the words floating apart instead of holding together as a phrase. Tighten it.
Single-line UI text, the labels, buttons, nav items, and form fields, lands at 1.0 to 1.2. None of it involves reading rhythm. The only job of line-height here is to leave enough vertical room that descenders and accents don’t clip.
Three variables, one decision
Line-height, font size, and measure are not independent. They settle reading comfort together.
A paragraph at max-width: 65ch, font-size: 1rem, and line-height: 1.6 is a comfortable read. Take that same 65-character column and drop the line-height to 1.2 and it turns claustrophobic, the lines packed tight enough that the return sweep starts landing on the wrong row. Nothing about the width changed. The leading alone moved it from comfortable to cramped.
The relationship runs in predictable directions. Wider columns need more leading, because the eye has farther to travel on the return and needs a cleaner vertical target to land on. Larger type needs less, because the size already supplies separation. When you adjust one of the three, check the other two.
Vertical rhythm, and the version of it that survives contact with the web
Vertical rhythm is the idea that every element on a page aligns to a consistent baseline grid. Pick a base unit, say 8px, and every line of text, every margin, every element height is a multiple of it. Print typography has done this for centuries, and on a fixed page it produces a page that feels measured and calm.
On the web it is close to impossible to hold. Images arrive at arbitrary heights. Content is dynamic and you don’t know the line count in advance. Fonts load asynchronously and shift metrics mid-render. Embedded media ignores your grid entirely. Chase a strict pixel-perfect baseline and you will spend real time fighting the platform for a result most readers never consciously register.
The practical answer keeps the intent and drops the literalism. Instead of forcing every element onto a shared pixel grid, you set a consistent line-height ratio per element type, define it once, and reference it everywhere through CSS custom properties. The rhythm comes from consistency of leading across the page, not from pixel alignment. That is the version of vertical rhythm worth implementing.
Leading tokens in the custom property system
Chapter 01 set up a :root block for the type scale. Leading tokens belong in the same block, named the same semantic way:
:root {
--text-xs: 0.8125rem; /* 13px — decorative metadata only */
--text-sm: 0.875rem; /* 14px — captions, secondary text */
--text-base: 1rem; /* 16px — body anchor */
--text-md: 1.25rem; /* 20px — lead text, prominent labels */
--text-lg: 1.5625rem; /* 25px — small headings (h4 range) */
--text-xl: 1.953rem; /* 31.25px — mid headings (h3 range) */
--text-2xl: 2.4375rem; /* 39px — large headings (h2 range) */
--text-3xl: 3rem; /* 48px — display, h1 */
--leading-body: 1.6;
--leading-heading: 1.2;
--leading-ui: 1.1;
}Three tokens cover most pages. Add a --leading-lead around 1.45 if the design has a distinct intro style.
Reference them where the element is styled:
body {
font-size: var(--text-base);
line-height: var(--leading-body);
}
h2 {
font-size: var(--text-2xl);
line-height: var(--leading-heading);
}
.button {
line-height: var(--leading-ui);
}One detail on placement: set line-height as close to the element as you can, not buried in a reset. The reset should carry one reasonable default and nothing more. Component styles override from there. A line-height pinned in a global reset is the same hidden coupling the unit-based example showed, just in a file you’re less likely to open.
What comes next
You now have a system with three parts: a named type scale, a controlled measure, and consistent leading. Every one of them is about arrangement: size, width, spacing. None of them touches the typefaces themselves. Chapter 04 takes that up, pairing a serif and a sans, and the question of which combinations hold together and which fight.