General

Vincent Bernat: CSS & vertical rhythm for text, images, and tables

Vertical rhythm aligns lines to a consistent spacing cadence down the page. It
creates a predictable flow for the eye to follow. Thanks to the rlh CSS unit,
vertical rhythm is now easier to implement for text.1 But illustrations
and tables can disrupt the layout. The amateur typographer in me wants to follow
Bringhurst’s wisdom:

Headings, subheads, block quotations, footnotes, illustrations, captions and
other intrusions into the text create syncopations and variations against the
base rhythm of regularly leaded lines. These variations can and should add
life to the page, but the main text should also return after each variation
precisely on beat and in phase.

― Robert Bringhurst, The Elements of Typographic Style

Text

Three factors govern vertical rhythm: font size, line height and
margin or padding. Let’s set our baseline with an 18-pixel font and a 1.5
line height:

html {
  font-size: 112.5%;
  line-height: 1.5;
}
h1, h2, h3, h4 {
  font-size: 100%;
}
html, body,
h1, h2, h3, h4,
p, blockquote,
dl, dt, dd, ol, ul, li {
  margin: 0;
  padding: 0;
}

CSS Values and Units Module Level 4 defines the rlh unit, equal to the
computed line height of the root element. All browsers support it since
2023
.2 Use it to insert vertical spaces or to fix the line height
when altering font size:3

h1, h2, h3, h4 {
  margin-top: 2rlh;
  margin-bottom: 1rlh;
}
h1 {
  font-size: 2.4rem;
  line-height: 2rlh;
}
h2 {
  font-size: 1.5rem;
  line-height: 1rlh;
}
h3 {
  font-size: 1.2rem;
  line-height: 1rlh;
}
p, blockquote, pre {
  margin-top: 1rlh;
}
aside {
  font-size: 0.875rem;
  line-height: 1rlh;
}

We can check the result by overlaying a grid4 on the content:

Screenshot of my website with a grid as an overlay and each line of text fitting on the grid

Using CSS rlh unit to set vertical space works well for text. You can display the grid using Ctrl+Shift+G.

If a child element uses a font with taller intrinsic metrics, it may stretch
the line’s box beyond the configured line height.5 A workaround
is to reduce the line height to 1. The glyphs overflow but don’t push the line
taller.

code, kbd {
  line-height: 1;
}

Responsive images

Responsive images are difficult to align on the grid because we don’t know their
height. CSS Rhythmic Sizing Module Level 1 introduces the block-step
property to adjust the height of an element to a multiple of a step unit. But
most browsers don’t support it yet.

With JavaScript, we can add padding around the image so it does not disturb
the vertical rhythm:

const targets = document.querySelectorAll(".lf-media-outer");
const adjust = (el, height) => {
  const rlh = parseFloat(getComputedStyle(document.documentElement).lineHeight);
  const padding = Math.ceil(height / rlh) * rlh - height;
  el.style.padding = `${padding / 2}px 0`;
};

targets.forEach((el) => adjust(el, el.clientHeight));
Screenshot of my website with a grid as an overlay and an image not breaking the vertical rhythm. Additional padding is visible before and after the image. The height of the image with padding is 216.

The image is snapped to the grid thanks to the additional padding computed with JavaScript. 216 is divisible by 27, our line height in this example.

As the image is responsive, its height can change. We need to wrap a resize
observer around the adjust() function:

const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const height = entry.contentBoxSize[0].blockSize;
    adjust(entry.target, height);
  }
});
for (const target of targets) {
  ro.observe(target);
}

Tables

Table cells could set 1rlh as their height but they would feel constricted.
Using 2rlh wastes too much space. Instead, we use incremental leading: we
align one in every five lines.

table {
  border-spacing: 2px 0;
  border-collapse: separate;
  th {
    padding: 0.4rlh 1em;
  }
  td {
    padding: 0.2rlh 0.5em;
  }
}

To align the elements after the table, we need to add some padding. We can
either reuse the JavaScript code from images or use a few lines of CSS that
count the regular rows and compute the missing vertical padding:

table:has(tbody tr:nth-child(5n):last-child)   { padding-bottom: 0.2rlh; }
table:has(tbody tr:nth-child(5n+1):last-child) { padding-bottom: 0.8rlh; }
table:has(tbody tr:nth-child(5n+2):last-child) { padding-bottom: 0.4rlh; }
table:has(tbody tr:nth-child(5n+3):last-child) { padding-bottom: 0 }
table:has(tbody tr:nth-child(5n+4):last-child) { padding-bottom: 0.6rlh; }

A header cell has twice the padding of a regular cell. With two regular rows,
the total padding is 2×2×0.2+2×0.4=1.6. We need to add 0.4rlh to reach
2rlh of extra vertical padding across the table.

Screenshot of my website with a grid as an overlay and a table following the vertical rhythm. Additional padding is visible after the table. The height of the table with padding is 405.

One line out of five is aligned to the grid. Additional padding is added after the table to not break the vertical rhythm. 405 is divisible by 27, our line height in this example.


None of this is necessary. But once you start looking, you can’t unsee it. Until
browsers implement CSS Rhythmic Sizing, a
bit of CSS wizardry and a touch of JavaScript is enough to pull it off. The main
text now returns after each intrusion “precisely on beat and in phase.” 🎼


  1. See “Vertical rhythm using CSS lh and rlh units” by Paweł
    Grzybek. ❦

  2. For broader compatibility, you can replace 2rlh with
    calc(var(--line-height) * 2rem) and set the --line-height custom
    property in the :root pseudo-class. I wrote a simple PostCSS
    plugin
    for this purpose. ❦

  3. It would have been nicer to compute the line height with
    calc(round(up, calc(2.4rem / 1rlh), 0) * 1rlh). Unfortunately, typed
    arithmetic is not supported by Firefox yet. Moreover, browsers support
    round() only since 2024. Instead, I coded a PostCSS
    plugin
    for this as well. ❦

  4. The following CSS code defines a grid tracking the line height:

    body::after {
      content: "";
      z-index: 9999;
      background: linear-gradient(180deg, #c8e1ff99 1px, transparent 1px);
      background-size: 20px 1rlh;
      pointer-events: none;
    }
    

    ❦

  5. See “Deep dive CSS: font metrics, line-height and vertical-align” by Vincent De Oliveira. ❦

AI Assistant
Context loaded: Vincent Bernat: CSS & vertical rhythm for text, images,