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 }, because rlh resolves against it. A unitless line-height is recommended so it scales with font-size.
  • Awareness of browser support. The lh and rlh units 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 the rem fallback 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
Spacing locked to a baseline grid using the lh unit Horizontal grid lines spaced one lh apart; text blocks and their margins each align to whole multiples of the line-height unit. Heading (1lh tall) Paragraph line Next block margin: 1lh margin: 1lh
Every block and its margin span whole multiples of 1lh, so text stays on the baseline grid.

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 stray lh where rlh was needed).
  • Computed values. Inspect a paragraph in DevTools and read the computed margin-bottom in pixels; it should equal root font-size × root line-height (27px in the example). Compare against an h2 margin — both should be whole multiples of that number.
  • Support fallback test. Toggle the @supports block off in DevTools and confirm the rem fallback keeps spacing visually identical, proving the degradation path works for older engines.

Common Pitfalls

  • Using lh for inter-block margins. An element's own lh differs from the root grid when its line-height differs; use rlh for spacing between blocks and reserve lh for spacing within an element.
  • Forgetting the fallback. Without an @supports guard, browsers before Chrome 109 / Safari 16.4 / Firefox 120 silently drop lh/rlh declarations, collapsing margins.
  • Mixing rhythm units with arbitrary px gaps. A single margin: 20px in the flow throws everything below it off the baseline grid; keep all vertical spacing in rhythm units.
  • Changing root line-height without updating the --rhythm fallback. The rem fallback is precomputed, so a new line-height must be reflected in --rhythm or the old browsers drift.
  • Ignoring the interaction with your type scale. Large heading sizes from a modular scale need their margins quantised to whole rlh multiples 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