Building a Baseline Grid with CSS Grid
This guide is part of the Line Height & Vertical Rhythm topic, itself inside the Typography Fundamentals & System Architecture area.
Most pages that "feel" typeset rather than merely styled share one trait: every line of text, regardless of font size, sits on a shared horizontal rhythm. Headings, body copy, captions and list items all land on multiples of a single baseline unit, so paragraphs that sit side by side in a two-column layout line up row for row, and vertical whitespace never looks arbitrary. Browsers give you no native "baseline grid" primitive, but grid-auto-rows combined with a fixed line-height unit gets you there without JavaScript, without a plugin, and without fighting line-height's tendency to add asymmetric leading above and below each glyph.
The core technique: define a rhythm unit (for example 1.5rem), build a CSS Grid whose implicit rows are all exactly that height, and force every text element's line-height to be an integer multiple of the unit. Because grid rows are hard-walled tracks, content that overflows its declared row count pushes into the next full unit rather than drifting by a few sub-pixels — which is exactly the failure mode that makes hand-tuned margins fall out of alignment after a font swap or a responsive breakpoint change.
Prerequisites
You should already be comfortable with basic CSS Grid track sizing (grid-template-columns, grid-auto-rows, grid-row: span N) and with how line-height is computed relative to a font's own metrics — the Font Metrics & Baseline Alignment guide covers ascent, descent, and why two fonts at the same font-size can render visually taller or shorter line boxes. It also helps to have picked a type scale already, since baseline-grid alignment interacts directly with whatever ratio you used to size headings — see Choosing a Modular Scale Ratio for UI Type if you have not settled on one. Finally, you need a rhythm unit that is a rational multiple of your base line-height, not an arbitrary round number picked for aesthetics alone.
Implementation
The pattern has three parts: a rhythm custom property, a grid container whose rows are locked to that property, and a rule that forces every typographic element's line box to consume a whole number of rows via grid-row: span N.
:root {
--rhythm: 1.5rem; /* one baseline unit = 24px at a 16px root */
}
.baseline-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-auto-rows: var(--rhythm);
align-items: start;
}
.baseline-grid > * {
margin: 0;
/* every child must occupy a whole number of rhythm rows */
}
.baseline-grid h1 {
font-size: 2.25rem;
line-height: calc(var(--rhythm) * 2); /* 48px line box */
grid-row: span 2;
}
.baseline-grid h2 {
font-size: 1.5rem;
line-height: var(--rhythm); /* 24px line box, one row */
grid-row: span 1;
}
.baseline-grid p {
font-size: 1rem;
line-height: var(--rhythm);
grid-row: span 1;
}
Line by line: --rhythm is the single source of truth for the unit — every other measurement in the layout should be calc()'d from it rather than hardcoded, so a designer can change the whole system by editing one variable. grid-auto-rows: var(--rhythm) tells the browser that any row it has to create implicitly (which is all of them here, since we never declare grid-template-rows) is exactly one rhythm unit tall. align-items: start matters because Grid's default stretch would otherwise vertically center or stretch content inside a taller spanned area, which breaks the alignment you just built — pinning content to the top of its row keeps the first line's baseline anchored near the row's top edge.
The h1 rule is the part people get wrong first: a font-size of 2.25rem does not automatically produce a line-height that is a clean multiple of 1.5rem. You choose the line-height explicitly as calc(var(--rhythm) * 2) — 48px — and then set grid-row: span 2 so the element consumes exactly two grid tracks. If your heading's natural line-height at that font-size would have been, say, 40px, you are intentionally adding extra leading to round up to the next full unit. That is the trade-off of a baseline grid: some elements get slightly more vertical space than their font's default metrics would produce, in exchange for perfect alignment across the whole page. This is the same class of problem the Vertical Rhythm with the CSS lh Unit guide addresses from the lh/rlh angle — that guide is the right next read if you want rhythm units expressed relative to an element's own computed line-height rather than a fixed root value.
Handling Multi-Column and Responsive Layouts
A single-column baseline grid is the easy case. The harder, and more common, real-world case is a two-column article layout where the left column's paragraph text and the right column's pull-quote or sidebar need to land on the same horizontal lines even though they contain different font sizes and different amounts of content. grid-auto-flow: row dense combined with per-column subgrids handles this without JavaScript reflow measurement:
.article-grid {
display: grid;
grid-template-columns: 2fr 1fr;
grid-auto-rows: var(--rhythm);
column-gap: 2rem;
}
.article-grid .col {
display: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
grid-row: 1 / -1;
}
@media (max-width: 640px) {
.article-grid {
grid-template-columns: 1fr;
/* rhythm unit itself can shrink at narrow viewports */
--rhythm: 1.375rem;
}
.article-grid .col {
grid-column: 1;
}
}
Each .col uses CSS Subgrid so its own children inherit the parent's exact row tracks instead of generating a fresh independent set — without grid-template-rows: subgrid, each column would compute its own implicit rows starting from its own top, and a shorter right-hand sidebar would not stay aligned to the left column's paragraph baselines once content length diverged. Subgrid is supported in current Chrome, Firefox and Safari; if you must support an older Safari release, fall back to matching grid-auto-rows values on sibling grids and accept that perfect row-for-row alignment across columns is only guaranteed while both columns have identical content lengths at each unit. At the 640px breakpoint the rhythm unit itself shrinks slightly — this is deliberate: fluid type scales (see Fluid Type Scale with clamp() for Responsive CSS) already shrink font sizes at narrow viewports, and the rhythm unit should shrink in lockstep or headings will stop landing on whole-row boundaries below the breakpoint.
Verification
Baseline-grid alignment is a visual regression, not a functional bug, so the check is a DevTools overlay rather than a script assertion. In Chrome DevTools, open the Elements panel, select the .baseline-grid container, and toggle on the grid overlay (the small grid badge next to the element, or the Layout panel's "Show grid" checkbox with "Show line numbers" enabled). With the overlay's row lines visible, you can visually confirm that every line of rendered text sits flush against a row boundary rather than floating mid-row.
For a repeatable check that survives font-loading race conditions, measure the actual rendered baseline position with getBoundingClientRect() after fonts are ready and assert it against an expected multiple of the rhythm unit:
async function assertOnBaseline(selector, rhythmPx) {
await document.fonts.ready;
const el = document.querySelector(selector);
const rect = el.getBoundingClientRect();
const containerTop = el.closest('.baseline-grid').getBoundingClientRect().top;
const offset = rect.top - containerTop;
const remainder = offset % rhythmPx;
// allow sub-pixel rounding from the browser's layout engine
console.assert(remainder < 0.5 || remainder > rhythmPx - 0.5,
`${selector} is ${remainder.toFixed(2)}px off the baseline grid`);
}
Running this after document.fonts.ready resolves matters: before the web font finishes loading, the fallback font's metrics may produce a slightly different line box height, and an assertion that runs too early will flag a false failure that resolves itself once the real font swaps in. If you also care about cumulative layout shift caused by that swap, the Debugging Font-Related Layout Shift guide covers isolating the swap-induced reflow with PerformanceObserver.
Common Pitfalls
- Setting
line-heightin unitless multiples of the font-size instead of the rhythm unit. A rule likeline-height: 1.4computes a different pixel value at every font-size in your scale, so a1.4line-height on anh2and a1.4line-height on body text will almost never both be multiples of the same rhythm unit. Always compute line-height from--rhythmwithcalc(), then choose a font-size that reads well at that fixed line-height — not the other way around. - Letting
paddingorborderon a grid child eat into its row height. Grid tracks are sized by content and explicit sizing rules, and a child withpadding: 0.5remplus a one-rowline-heightwill either overflow its track (breaking alignment for everything below it) or force the whole implicit row taller (breaking the fixed-unit assumption everywhere). Keep padding on an inner wrapper element, not on the grid item itself, or size it in exact rhythm-unit increments. - Applying
align-items: stretch(the Grid default) instead ofstart. Stretch vertically centers or stretches a short line of text inside a multi-row span, which visually detaches the text baseline from the row boundary the overlay shows. This is the single most common reason a baseline grid "looks right in the inspector but wrong on screen." - Rounding the rhythm unit to a value that is not an integer number of device pixels at common zoom levels. A
1.5remunit at a 16px root is 24px, which divides cleanly, but a unit like1.45remproduces sub-pixel row heights that different browsers round differently, causing visible drift over many rows. Pick a rhythm unit, in pixels, that has small integer factors. - Forgetting that variable fonts and OpenType feature settings (e.g. optical sizing) can shift a font's own ascent/descent metrics. Toggling
font-optical-sizingor a variable weight axis mid-render can change the natural line box height under the hood even though your CSSline-heightvalue stays fixed; the Optical Sizing & Variable Font Axes guide explains how to keep those metrics stable when you rely on a locked-down grid.
Frequently Asked Questions
Does a baseline grid conflict with line-height: normal or browser default line-heights?
Yes — normal resolves to a font-specific ratio (commonly somewhere between 1.1 and 1.3 depending on the typeface's own metrics), which is almost never an exact multiple of your chosen rhythm unit. Every element that participates in the grid needs an explicit line-height computed from --rhythm, and any element outside the grid (a modal, a tooltip, an unrelated widget) can safely keep normal.
Can I use grid-template-rows: repeat(auto-fill, var(--rhythm)) instead of grid-auto-rows?
Only if you know the total number of rows in advance, since repeat(auto-fill, …) needs a definite grid container height to compute how many tracks to generate; for flowing text content of unknown length, grid-auto-rows (which sizes rows created implicitly, on demand, as content is placed) is the correct tool and is what real-world baseline-grid implementations use.
What happens if a paragraph's content wraps to more lines than its declared grid-row: span N?
The browser does not clip it — Grid items are allowed to overflow their assigned tracks by default, so the paragraph's box grows past its span boundary and every element placed after it in document order shifts down by whatever overflow amount was introduced, breaking alignment from that point on. In practice this means you either need to constrain column width so your copy's line count is predictable, or calculate span dynamically from measured content rather than hardcoding it per element type.
Is a baseline grid worth the complexity for a typical marketing or blog page?
For a single column of body copy with occasional headings, a well-chosen line-height and consistent margin scale usually looks correct without an explicit grid — the technique pays off specifically when you have multi-column layouts, side-by-side cards, or a design system where perfect cross-column alignment is a stated requirement, such as editorial or documentation sites with pull-quotes running alongside body text.