Setting the Web on a Baseline Grid
View

Set two columns of text side by side, a caption and the body copy next to it. If their lines sit at different heights, the eye notices before you can say what is wrong. In a magazine this does not happen: every line on the spread sits on one shared, invisible ruler. On the web it happens almost every time.

This post covers why, and how this site puts every baseline (headings, captions, tables, buttons) on one 8 px step. No JavaScript and no offsets tuned by eye: one CSS mixin that could not have been written two years ago.
What a Baseline Grid Is in Print
The baseline is the line letters stand on: the bottom of x, n and o. A baseline grid is a set of evenly spaced horizontal lines across the page, and every baseline of text lands on one of them.
In print this is an old craft. Compositors measured lines in whole body sizes and leads, so text on both sides of a sheet matched by itself and lines did not show through between the lines on the reverse. The Swiss school turned it into a system: in Müller-Brockmann's grids a module's height is a whole number of body lines, and the top of an image meets the cap height in the next column.
In InDesign it is one checkbox. Set a Baseline Grid (say, 12 pt), turn on Align to Baseline Grid in the paragraph style, and every line snaps to the nearest grid line. The rules are simple:
- every style's leading is a multiple of the grid step;
- space before and after paragraphs is a multiple too;
- a large heading spans two or three grid lines, but its baseline still sits on the grid;
- images and panels are a whole number of steps tall.
The result is not visible as a technique; it reads as order. A page stays calm with five type sizes on it.
Why the Web Does Not Do This by Itself
A browser aligns boxes, not letters. The top of a text block is the top of its first line box, not the top of its capitals and not the baseline.
Three things make the baseline's position unpredictable:
- Half-leading The difference between
line-heightand the font's height is split and added above and below each line. Text at 16/24 gets invisible air above its first line that the design file never had. - Font metrics Where the baseline sits inside the line depends on the font file's ascender and descender. Two typefaces at the same size put it in different places.
- No snapping CSS has no Align to Baseline Grid. The CSS Line Grid draft (
line-grid,line-snap) has been a working draft since 2014 and no browser implemented it.

Hence the most common designer complaint: "the gap is 24 in Figma but looks bigger in the browser." The gap is 24; two half-leadings were added to it, one under the first block and one over the next.
From Magic Numbers to text-box-trim
For years it ran on magic numbers tuned to one font. Now the browser knows where the top and bottom of the letters are.
| Approach | How it works | Catch |
|---|---|---|
| Rhythm only | line-height and every gap are multiples of the module | Lines keep an even step, but the baselines themselves sit off the grid |
| Basehold and similar | position: relative; top: Npx plus a reduced margin-bottom, numbers tuned per size | A new font or size means a new table of offsets |
| Capsize | Negative margins on ::before and ::after computed from font metrics | Accurate, but the metrics are hard-coded; a fallback font drifts |
text-box-trim | The browser trims half-leading to the cap height and the baseline | Needs a fallback for older browsers |
The new property looks like this:
h1 {
text-box: trim-both cap alphabetic;
}
The block now starts at the top of the capitals and ends at the baseline of its last line, like a text frame in InDesign. A 24 px gap between two such blocks is 24 px between letters, not between invisible boxes. Safari has it from 18.2, Chrome from 133, and Firefox from 154 (August 2026), which made it Baseline.
Trimming alone does not give you a grid. It removes the excess but does not put the baseline on a step of the module: cap height is rarely a whole number of modules. That is the job of two other recent additions to CSS: the round() function and the cap unit.
How This Site Does It
The system rests on three rules: an 8 px module, every vertical size a multiple of it, and every text block starting and ending on a grid line.
One Mixin
@mixin on-baseline($top: 0px) {
padding-top: $top;
@supports (text-box: trim-both cap alphabetic) {
text-box: trim-both cap alphabetic;
padding-top: calc(#{$top} + round(up, 1cap, 8px) - 1cap);
}
}
Step by step:
text-box: trim-both cap alphabeticmakes the block run from cap height to baseline, so the first baseline sits exactly1capbelow the top of the block.round(up, 1cap, 8px)is the next multiple of 8 up. For a cap height of 11.4 px that is 16 px.- The difference goes into
padding-top: 16 − 11.4 = 4.6 px. The first baseline drops exactly onto a grid line. - The bottom of the block is trimmed to the baseline, which is on the grid, so the next block starts on the grid too, provided the gap is a multiple of 8.

The cap unit takes the cap height from the font that actually loaded. No metrics table as in Capsize: change the font, or let a fallback kick in, and the formula recalculates itself.
Leading in Multiples of the Module
Once the first baseline is on the grid, the rest follow as long as line-height is a multiple of 8. Captions are 16 px, body copy 24 px, row titles 32 px. Fluid type uses the same round():
font-size: clamp(2rem, 4.5vw, 4rem);
line-height: round(1.08em, 8px);
The font size scales freely; the leading steps in eights. Spacing sits on the module too: --space-copy 16, --space-block 24, --space-group 32, header height 64.
Rules Come out of the Leading
A 1 px rule pushes everything below it down a pixel, so the pixel comes out of the neighbouring padding: a table row under its rule is padding: calc(1rem - 1px) 0 1rem. The rule takes the place of some of the air, and the rhythm holds.
Images Round to 8
An image with height: auto gets an arbitrary height and shifts the text below it. In Media.vue the wrapper is a size container, and the height is computed from its width and rounded:
.media { container-type: inline-size; }
.media img {
height: round(calc(100cqw * var(--media-ratio)), 8px);
}
The proportion drifts by a fraction of a pixel; the text under the image stays on the grid. Logos have fixed heights of 40 and 96 px.
Rows Share One Baseline
A row in the work list holds a number (12 px), a title (32 px) and a description (16 px). A grid row with align-items: baseline puts them on one line, as in a magazine table, instead of pinning them to the top edge.

The Grid on a Key
Checking this by eye is pointless. On this site the g key (or ?grid in the address) turns on an overlay: cyan lines every 8 px and red bands for the 14 columns. It is one repeating-linear-gradient that follows the scroll. A baseline is on the grid when the letters stand on a line, visible at a glance without the inspector.
Where the Grid Breaks
A perfect grid is not yet possible in a browser. Here is what had to be worked around:
- Flex and grid Only block containers trim:
text-box-trimdoes nothing on flex and grid containers. In a link with an arrow, the text<span>is trimmed and the arrow aligns by its bottom edge. - Anonymous text Text after a
<strong>in Markdown is not its own element, so there is nothing to trim. In those rows the number and title are positioned absolutely, and the row itself is a plain block. - Form controls An
<input>cannot be trimmed, so it pads 7 px above a 24 px line. The CAPTCHA widget gets a 72 px box. - Inline strut A standalone link with
display: inlinedrags its parent's strut along.display: flex; width: fit-contentfixes it. - Older browsers Without
text-box-trimthe branch outside@supportsapplies: baselines are offset, but line boxes still sit on the module and the rhythm holds.
Conclusion
A baseline grid on the web no longer needs magic numbers. Three tools are enough: text-box-trim, the cap unit and round(), plus the discipline to keep every vertical size a multiple of the module.
The valuable part is not the technique but the rule from print: distance is measured from letters to letters, not from box to box. Once CSS thinks the same way, a site starts to look typeset rather than laid out. Turn the grid on right here: View in the left margin, the g key or ?grid in the address.