Cached at:
05/12/26, 07:42 AM
# CSS & vertical rhythm for text, images, and tables
Source: [https://vincent.bernat.ch/en/blog/2026-css-vertical-rhythm](https://vincent.bernat.ch/en/blog/2026-css-vertical-rhythm)
## Vincent BernatApril 22, 2026
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](https://vincent.bernat.ch/en/blog/2026-css-vertical-rhythm#sidenote-pawel)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](https://en.wikipedia.org/wiki/The_Elements_of_Typographic_Style)
## Text[\#](https://vincent.bernat.ch/en/blog/2026-css-vertical-rhythm#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](https://www.w3.org/TR/css-values-4/)defines the`rlh`unit, equal to the computed line height of the root element\. All browsers support it[since 2023](https://webstatus.dev/features/rlh)\.[2](https://vincent.bernat.ch/en/blog/2026-css-vertical-rhythm#sidenote-postcss)Use it to insert vertical spaces or to fix the line height when altering font size:[3](https://vincent.bernat.ch/en/blog/2026-css-vertical-rhythm#sidenote-calc)
```
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 grid[4](https://vincent.bernat.ch/en/blog/2026-css-vertical-rhythm#sidenote-grid)on the content:

Using CSS`rlh`unit to set vertical space works well for text\. You can display the grid usingCtrl\+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](https://vincent.bernat.ch/en/blog/2026-css-vertical-rhythm#sidenote-line-height)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[\#](https://vincent.bernat.ch/en/blog/2026-css-vertical-rhythm#responsive-images)
Responsive images are difficult to align on the grid because we don’t know their height\.[CSS Rhythmic Sizing Module Level 1](https://www.w3.org/TR/css-rhythm-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));
```

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[\#](https://vincent.bernat.ch/en/blog/2026-css-vertical-rhythm#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](https://markboulton.co.uk/journal/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\.

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](https://www.w3.org/TR/css-rhythm-1/), 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\.” 🎼