Line Height (Leading) & Vertical Rhythm
Mastering line height and vertical rhythm transforms walls of text into breathing, readable prose — and makes entire page layouts feel deliberate rather than accidental.
7 min read
The full lesson
Every typographic decision lives on two axes. The horizontal axis covers letter spacing and line length. The vertical axis covers how lines stack on top of each other. Line height — called leading in print (named after the strips of lead typesetters once wedged between lines) — is the most powerful lever on that vertical axis.
Get it wrong and even a beautiful typeface feels cramped or eerily floaty. Get it right and readers glide through paragraphs without noticing the mechanics at all.
Vertical rhythm extends this idea beyond a single text block to the whole page. When spacing, type sizes, and layout heights all share a common grid unit, the eye traces invisible horizontal rails through the composition. The result feels engineered rather than assembled.
What Line Height Actually Controls
The CSS line-height property sets the height of each line box — the invisible rectangle that wraps one line of text. The extra space between the line box height and the actual glyphs is split equally above and below the characters. This is called half-leading. The visible gap between two lines of text is always the sum of the half-leading below one line and the half-leading above the next.
A unitless value (e.g. line-height: 1.5) multiplies the ratio against the element’s current font size. It inherits as a ratio, not a fixed pixel value. This is almost always what you want.
/* Unitless ratio — preferred */
p {
font-size: 1rem;
line-height: 1.5; /* = 24px at 16px base */
}
/* Pixel value — avoid for body text; doesn't scale with font size changes */
p {
line-height: 24px;
}
/* Em value — computes correctly but a common trap in inheritance */
p {
line-height: 1.5em; /* inherits computed px, not the ratio */
}
Choosing the Right Value
There is no single “correct” line height. The right value depends on three things: font size, line length (measure), and typeface characteristics (x-height, tracking, and weight).
Body text
WCAG 2.2 Success Criterion 1.4.12 (Text Spacing) requires that content does not lose function when line height is set to at least 1.5 times the font size. Treat 1.5 as a reasonable floor, not a magic number. In practice, body text typically lands between 1.4 and 1.8 depending on the typeface:
| Context | Typical range | Notes |
|---|---|---|
| Long-form article body | 1.55 – 1.75 | Wider measure tolerates higher leading |
| UI body copy (cards, tooltips) | 1.4 – 1.55 | Shorter lines, tighter reading context |
| Code blocks | 1.5 – 1.7 | Monospace fonts often have lower x-height |
| Mobile body text | 1.5 – 1.65 | Shorter line lengths read well with mid-range leading |
Headings
Headings are set larger and read more quickly. The eye takes them in almost as a single unit rather than tracking word by word. Tight leading actually looks better at display sizes:
h1(display / hero):line-height: 1.05 – 1.15h2(section title):line-height: 1.15 – 1.25h3(subsection):line-height: 1.25 – 1.35
A common mistake is leaving the browser default (line-height: normal, which resolves to roughly 1.2) on all heading levels. For small headings that wrap to two or three lines, 1.2 is too tight. For large single-line display text, it can be appropriate.
The measure connection
Leading and line length are coupled. A narrow column (45–60 characters) with leading of 1.8 feels like a double-spaced document. A very wide measure (90+ characters) with leading of 1.4 is fatiguing because the eye struggles to find the start of the next line. As measure increases, leading should increase proportionally — roughly +0.05 for every 10-character increase beyond 60ch.
Vertical Rhythm: From Lines to Page
Vertical rhythm means aligning vertical spacing across your entire layout to a baseline grid or a consistent spacing unit. When done well, elements feel like they belong to the same system even when they are far apart.
The classical approach — snapping every element’s baseline to a 4px, 8px, or custom grid — works well in print. On the web it is hard to enforce perfectly. Images, varied font metrics, and dynamic content all break strict baseline alignment. The modern, pragmatic approach focuses on a consistent spacing scale instead of pixel-perfect snapping.
Defining a spacing unit
Pick a base unit that harmonizes with your base line height:
:root {
--font-size-base: 1rem; /* 16px */
--line-height-base: 1.5; /* 24px computed */
--space-unit: 0.25rem; /* 4px — quarter of the line height */
}
With a 24px line height, multiples of 4px (4, 8, 12, 16, 20, 24, 32, 40, 48…) keep vertical spacing visually related to text rhythm. This is why most design systems — Material Design, Base Web, Radix — use a 4px base space unit.
Paragraph and section spacing
Spacing between paragraphs should signal a new thought without breaking the sense of continuous flow. A reliable rule of thumb: margin-block-end on paragraphs should equal roughly one full line height (i.e. 1em in relative terms, since the em of the element equals its font size).
p {
margin-block-end: 1em; /* exactly one line worth of space */
}
h2 {
margin-block-start: 2em; /* two lines above a major section */
margin-block-end: 0.5em; /* half a line between heading and following text */
}
The key insight: heading margins should connect the heading to the content that follows it, not to the content above it. margin-block-start should be larger. margin-block-end should be smaller. The heading belongs to the section it introduces.
CSS Techniques and Modern Implementation
Fluid line height with clamp()
Just as fluid type sizes use clamp(), fluid line height lets you taper leading as text scales up:
h1 {
font-size: clamp(2rem, 5vw, 4rem);
/* Tighter leading at large sizes, slightly looser at small */
line-height: clamp(1.05, 1.1 + 0.2vw, 1.2);
}
This prevents an awkward situation: a heading that looks perfect at 64px on desktop but feels uncomfortably loose at 32px on mobile when the same unitless ratio is applied to both.
Design tokens for spacing
Under the W3C DTCG token format, line height belongs in a dedicated token group. A three-tier structure keeps it maintainable:
{
"lineHeight": {
"tight": { "$value": 1.1, "$type": "number" },
"snug": { "$value": 1.25, "$type": "number" },
"normal": { "$value": 1.5, "$type": "number" },
"relaxed": { "$value": 1.625, "$type": "number" },
"loose": { "$value": 1.75, "$type": "number" }
}
}
Semantic tokens then reference those primitives:
{
"text-body": { "$value": "{lineHeight.normal}", "$type": "number" },
"text-heading-display": { "$value": "{lineHeight.tight}", "$type": "number" }
}
This approach — primitive tokens feeding semantic tokens — means you can change the base scale globally without hunting for hardcoded values across stylesheets.
Variable fonts
Variable fonts with a wdth (width) or custom axes can subtly affect the visual impression of spacing. At a narrow width, compressed glyphs may need slightly tighter leading. At wide widths, they may benefit from slightly more. If your design system supports variable fonts, consider linking line-height adjustments to font-variation-settings axis values via CSS custom properties.
WCAG Requirements and Accessibility
WCAG 2.2 SC 1.4.12 (Text Spacing) is the primary accessibility reference point. It requires that no content or functionality is lost when all of the following are applied at the same time:
- Line height set to at least 1.5 times the font size
- Spacing after paragraphs set to at least 2 times the font size
- Letter spacing set to at least 0.12 times the font size
- Word spacing set to at least 0.16 times the font size
This is a minimum non-breakage criterion — not a design recommendation. Meeting it means your layout does not collapse when users apply custom stylesheets. It does not mean 1.5 is the optimal value. For many typefaces and contexts, 1.6 or 1.7 is the better design choice.
Users with dyslexia, low vision, or cognitive disabilities commonly override browser defaults with custom stylesheets that increase line height. Build layouts that accommodate this gracefully: avoid overflow: hidden on text containers and use min-height rather than height.
Common Mistakes and How to Fix Them
Do
Use unitless line-height values (e.g. 1.5) on all text elements so the ratio is inherited correctly at every font size. Set heading line heights explicitly and scale them tighter as size increases. Build a spacing scale from a unit tied to your base line height (multiples of 4px or 8px). Use margin-block-start larger than margin-block-end on headings to connect them to the content they introduce.
Don't
Don’t use em or px values for line-height on containers with nested text at different sizes — children inherit the computed value, not the ratio. Don’t rely on the browser default (normal, ~1.2) for body text — it’s too tight for comfortable reading. Don’t apply the same line-height to body text and large display headings. Don’t ignore WCAG 1.4.12 by using overflow: hidden on text containers with fixed heights.
Outdated habits still circulating
Fixed px line heights remain common in older codebases. They break as soon as a user changes their browser font size or a heading wraps to a second line. Replace them with unitless ratios.
Copy-pasting line-height: 1.5 everywhere is better than nothing, but it ignores the size-leading relationship. Large headings with 1.5 leading look like a double-spaced draft.
Pixel-perfect baseline grids in web UI — borrowed from print design — require constant hacks the moment images, icons, or user-generated content appears. Use a consistent spacing scale instead. It delivers 80% of the visual benefit at 5% of the maintenance cost.
Practical Checklist
Before shipping any text-heavy screen, verify:
- Body text line height is 1.4 or greater (aim for 1.5–1.65 for article-length content).
- All heading levels have explicit, progressively tighter line-height values.
- Paragraph
margin-block-endis approximately 1em (one line worth of space). - Section headings have more space above them than below.
- No text container uses
overflow: hiddenwith a fixedheight— usemin-height. - Spacing values are multiples of your base spacing unit (4px or 8px).
- Layout survives the WCAG 1.4.12 Text Spacing test without content loss.