Vertical Rhythm with the CSS lh Unit: Baseline Grids in Pure CSS
This guide is part of the Line-Height & Vertical Rhythm section within the Typography Fundamentals & System Architecture blueprint. It resolves one specific technique: using the lh and rlh length units to express spacing as multiples of the line-height, so every margin and gap snaps to a single baseline grid without magic numbers.
Problem Statement
Vertical rhythm means every block of text sits on a consistent baseline grid: the distance between lines, and the spacing between paragraphs and headings, are all multiples of one rhythm unit. Traditionally you approximate this with rem or em margins hand-tuned to roughly equal your line-height — but the moment line-height changes, or a heading uses a different one, the arithmetic breaks and the grid drifts. The lh unit makes the relationship exact: 1lh is the computed line-height of the current element, and 1rlh is the line-height of the root element. Expressing margins in lh/rlh means spacing is defined in terms of the rhythm itself, so it stays locked to the grid automatically even when font sizes or line-heights change.
Prerequisites
- A defined root line-height, e.g.
:root { line-height: 1.5 }, becauserlhresolves against it. A unitless line-height is recommended so it scales with font-size. - Awareness of browser support. The
lhandrlhunits shipped in Chrome 109 (Jan 2023), Safari 16.4 (Mar 2023), and Firefox 120 (Nov 2023). They are now Baseline, but if you support older releases you need theremfallback shown below. - A consistent base font-size so the rhythm unit is predictable across the document.
| Unit | What it resolves to | Chrome | Safari | Firefox |
|---|---|---|---|---|
lh |
line-height of the current element | 109 | 16.4 | 120 |
rlh |
line-height of the root element | 109 | 16.4 | 120 |
Implementation
The core technique: set spacing properties in lh (for rhythm relative to the element's own line-height) or rlh (for rhythm locked to the document-wide grid). Use rlh for inter-block spacing so headings and paragraphs share one grid even though their own line-heights differ.
Baseline-grid spacing with lh and rlh units
:root {
font-size: 1.125rem; /* ~18px base */
line-height: 1.5; /* the rhythm unit: 1rlh = 1.5 x 18px = 27px */
}
body {
/* paragraphs and headings flow on a 1rlh grid */
margin: 0;
}
p {
margin-block: 0 1rlh; /* trailing gap = exactly one root rhythm unit */
}
h2 {
font-size: 1.5rem; /* bigger text, different OWN line-height... */
line-height: 1.2;
/* ...but spacing uses rlh, so it stays on the SHARED baseline grid */
margin-block: 2rlh 1rlh;
}
blockquote {
/* indent and pad in lh so the quote's internal rhythm matches itself */
padding-block: 1lh;
border-inline-start: 0.25rem solid currentColor;
margin-block: 1rlh;
}
.stack > * + * {
/* a utility: every stacked sibling separated by one rhythm unit */
margin-block-start: 1rlh;
}
The annotated reasoning: 1rlh equals the root line-height (1.5 × 18px = 27px here), so any margin-block expressed in rlh is a whole number of grid rows. The h2 is the load-bearing example — it has a different line-height (1.2) for its own larger text, yet its surrounding margins use rlh, not lh. That distinction is the whole trick: if the heading's margins used 1lh, they would be 1.2 × 24px = 28.8px, off-grid relative to the body's 27px rows. By using rlh for inter-block spacing and reserving lh for intra-element padding (like the blockquote, whose internal padding should track its own line-height), every gap between blocks remains a clean multiple of the single document rhythm. The .stack utility generalises this: any vertical stack separates children by exactly one rhythm unit, the modern lobotomized-owl pattern expressed in baseline units.
Defensive Variant: rem Fallback via @supports
For browsers predating Chrome 109 / Safari 16.4 / Firefox 120, lh/rlh are invalid and the declaration is dropped, collapsing your spacing. The defensive pattern declares a rem approximation first (always applied), then overrides with the rlh version inside an @supports query so only capable browsers use the precise unit.
Progressive enhancement: rem baseline first, lh/rlh when supported
:root {
font-size: 1.125rem;
line-height: 1.5;
/* precompute the rhythm unit as a rem value for the fallback:
1.5 (line-height) x 1.125rem = 1.6875rem */
--rhythm: 1.6875rem;
}
/* 1. Fallback applied to every browser. */
p { margin-block: 0 var(--rhythm); }
h2 { margin-block: calc(var(--rhythm) * 2) var(--rhythm); }
/* 2. Capable browsers replace the approximation with exact rlh,
which stays correct even if line-height changes at runtime. */
@supports (margin: 1rlh) {
p { margin-block: 0 1rlh; }
h2 { margin-block: 2rlh 1rlh; }
}
The fallback --rhythm custom property hard-codes the rhythm as a rem value (1.5 × 1.125rem), which is correct as long as the root line-height does not change. The @supports (margin: 1rlh) query then upgrades capable browsers to the live rlh unit, which has the advantage of recomputing automatically if line-height changes (for example via a user stylesheet or a density toggle). This way old browsers still get an on-grid layout from the static rem math, and modern browsers get the self-correcting rlh version, with no JavaScript and no broken spacing anywhere.
Verification
- Baseline overlay. Add a temporary debug background of repeating grid lines one rhythm unit apart:
body { background-image: repeating-linear-gradient(to bottom, transparent 0, transparent calc(1rlh - 1px), rgba(255,0,0,.3) 1rlh); }. Every line of text and every block edge should land on or between the red lines consistently. Drift means a margin is off-grid (often a straylhwhererlhwas needed). - Computed values. Inspect a paragraph in DevTools and read the computed
margin-bottomin pixels; it should equalroot font-size × root line-height(27px in the example). Compare against anh2margin — both should be whole multiples of that number. - Support fallback test. Toggle the
@supportsblock off in DevTools and confirm theremfallback keeps spacing visually identical, proving the degradation path works for older engines.
Common Pitfalls
- Using
lhfor inter-block margins. An element's ownlhdiffers from the root grid when its line-height differs; userlhfor spacing between blocks and reservelhfor spacing within an element. - Forgetting the fallback. Without an
@supportsguard, browsers before Chrome 109 / Safari 16.4 / Firefox 120 silently droplh/rlhdeclarations, collapsing margins. - Mixing rhythm units with arbitrary
pxgaps. A singlemargin: 20pxin the flow throws everything below it off the baseline grid; keep all vertical spacing in rhythm units. - Changing root line-height without updating the
--rhythmfallback. Theremfallback is precomputed, so a new line-height must be reflected in--rhythmor the old browsers drift. - Ignoring the interaction with your type scale. Large heading sizes from a modular scale need their margins quantised to whole
rlhmultiples or they punch holes in the rhythm.
FAQ
What is the difference between lh and rlh?
1lh equals the computed line-height of the current element, so it changes per element when line-heights differ. 1rlh equals the line-height of the root element and is therefore constant across the whole document — making it the right unit for a single shared baseline grid. Use rlh for inter-block rhythm and lh for spacing that should track an element's own line-height.
Can I rely on lh and rlh in production today?
Yes for evergreen browsers — they shipped in Chrome 109, Safari 16.4, and Firefox 120 and are now Baseline. For audiences on older releases, pair them with a rem-based fallback inside an @supports (margin: 1rlh) query as shown, so unsupported browsers still get an on-grid layout.
Related
- Line-Height & Vertical Rhythm — the parent section on leading and baseline grids.
- Type Scale & Modular Grids — quantising scale steps to the rhythm unit.
- A Modular Type Scale with CSS Custom Properties — deriving the sizes that sit on this grid.