{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreigxbvcn7y4gntlaximpbdhodt2wjyh5c7zadtd2vebb7nf4zurnfu",
    "uri": "at://did:plc:46ti67tc37qcmwp2vaynk6fq/app.bsky.feed.post/3mk4v432ujtx2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreic4cfco3rxd4gasjauwfoagb66mmpv3i3qyifzcp5i63thybqd53u"
    },
    "mimeType": "image/jpeg",
    "size": 60054
  },
  "path": "/en/blog/2026-css-vertical-rhythm",
  "publishedAt": "2026-04-23T01:42:49.149Z",
  "site": "https://vincent.bernat.ch",
  "tags": [
    "1",
    "The Elements of Typographic Style",
    "Text",
    "Responsive images",
    "Tables",
    "CSS Values and Units Module Level 4",
    "since\n2023",
    "2",
    "3",
    "4",
    "5",
    "CSS Rhythmic Sizing Module Level 1",
    "incremental leading",
    "CSS Rhythmic Sizing",
    "Vertical rhythm using CSS lh and rlh units",
    "",
    "simple PostCSS\nplugin",
    "not supported by Firefox yet",
    "since 2024",
    "PostCSS\nplugin",
    "Deep dive CSS: font metrics, line-height and vertical-align"
  ],
  "textContent": "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:\n\n> 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.\n>\n> ― _Robert Bringhurst_ , The Elements of Typographic Style\n\n  * Text\n  * Responsive images\n  * Tables\n\n\n\n# Text\n\nThree 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:\n\n\n    html {\n      font-size: 112.5%;\n      line-height: 1.5;\n    }\n    h1, h2, h3, h4 {\n      font-size: 100%;\n    }\n    html, body,\n    h1, h2, h3, h4,\n    p, blockquote,\n    dl, dt, dd, ol, ul, li {\n      margin: 0;\n      padding: 0;\n    }\n\n\nCSS 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\n2023.2 Use it to insert vertical spaces or to fix the line height when altering font size:3\n\n\n    h1, h2, h3, h4 {\n      margin-top: 2rlh;\n      margin-bottom: 1rlh;\n    }\n    h1 {\n      font-size: 2.4rem;\n      line-height: 2rlh;\n    }\n    h2 {\n      font-size: 1.5rem;\n      line-height: 1rlh;\n    }\n    h3 {\n      font-size: 1.2rem;\n      line-height: 1rlh;\n    }\n    p, blockquote, pre {\n      margin-top: 1rlh;\n    }\n    aside {\n      font-size: 0.875rem;\n      line-height: 1rlh;\n    }\n\n\nWe can check the result by overlaying a grid4 on the content:\n\nUsing CSS `rlh` unit to set vertical space works well for text. You can display the grid using `Ctrl`+`Shift`+`G`.\n\nIf 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.\n\n\n    code, kbd {\n      line-height: 1;\n    }\n\n\n# Responsive images\n\nResponsive 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.\n\nWith JavaScript, we can add padding around the image so it does not disturb the vertical rhythm:\n\n\n    const targets = document.querySelectorAll(\".lf-media-outer\");\n    const adjust = (el, height) => {\n      const rlh = parseFloat(getComputedStyle(document.documentElement).lineHeight);\n      const padding = Math.ceil(height / rlh) * rlh - height;\n      el.style.padding = `${padding / 2}px 0`;\n    };\n\n    targets.forEach((el) => adjust(el, el.clientHeight));\n\n\nThe 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.\n\nAs the image is responsive, its height can change. We need to wrap a resize observer around the `adjust()` function:\n\n\n    const ro = new ResizeObserver((entries) => {\n      for (const entry of entries) {\n        const height = entry.contentBoxSize[0].blockSize;\n        adjust(entry.target, height);\n      }\n    });\n    for (const target of targets) {\n      ro.observe(target);\n    }\n\n\n# Tables\n\nTable 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.\n\n\n    table {\n      border-spacing: 2px 0;\n      border-collapse: separate;\n      th {\n        padding: 0.4rlh 1em;\n      }\n      td {\n        padding: 0.2rlh 0.5em;\n      }\n    }\n\n\nTo 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:\n\n\n    table:has(tbody tr:nth-child(5n):last-child)   { padding-bottom: 0.2rlh; }\n    table:has(tbody tr:nth-child(5n+1):last-child) { padding-bottom: 0.8rlh; }\n    table:has(tbody tr:nth-child(5n+2):last-child) { padding-bottom: 0.4rlh; }\n    table:has(tbody tr:nth-child(5n+3):last-child) { padding-bottom: 0 }\n    table:has(tbody tr:nth-child(5n+4):last-child) { padding-bottom: 0.6rlh; }\n\n\nA 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.\n\nOne 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.\n\n* * *\n\nNone 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.” 🎼\n\n* * *\n\n  1. See “Vertical rhythm using CSS lh and rlh units” by Paweł Grzybek. ❦\n\n  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\nplugin for this purpose. ❦\n\n  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\nplugin for this as well. ❦\n\n  4. The following CSS code defines a grid tracking the line height:\n\n         body::after {\n           content: \"\";\n           z-index: 9999;\n           background: linear-gradient(180deg, #c8e1ff99 1px, transparent 1px);\n           background-size: 20px 1rlh;\n           pointer-events: none;\n         }\n\n\n❦\n\n  5. See “Deep dive CSS: font metrics, line-height and vertical-align” by Vincent De Oliveira. ❦\n\n\n",
  "title": "Vincent Bernat: CSS & vertical rhythm for text, images, and tables",
  "updatedAt": "2026-04-22T19:48:10.000Z"
}