I built a tool that measures whether a page sits on a baseline grid, pointed it at the reference design systems to see whether anyone had one, and included my own site in the table because a survey that leaves out the surveyor is marketing. Nobody had a grid, and that part I expected.
What I didn't expect was the other column. Percentages tell you how far off a page is. The count of distinct drift values tells you whether it's off in an orderly way, and that's the number that describes a design system rather than a stylesheet. Five distinct values across a whole homepage means the type scale and the spacing scale agree with each other. Thirty means they're having different conversations.
GOV.UK, Shopify Polaris and Material Design all sit at five. My site sat at ninety. Last in the table, three times worse than the next worst, on the one measurement I'd built the instrument for.
There's a particular flavour of embarrassment in building the ruler, publishing the results, and coming last on your own ruler.
So this is what ninety was actually made of. It was three things, none of them the thing I assumed, and I found all three by measuring rather than by reading my own CSS, which is a habit this project keeps rewarding.
One: a token being quietly put back
I'd already rewritten the token file to put the spacing scale on the grid and snap every leading. So my first assumption was that the remaining mess was the type scale being too varied. It wasn't. The site has eleven distinct font sizes and three families, which is at most thirty-three combinations, and there were ninety drift values. The maths didn't work, so the cause was somewhere else.
Somewhere else was a media query, in a different file, at every width above 768px, setting three tokens back to what they had been before.
/* styles.css, six thousand lines from the token file */
@media (min-width: 768px) {
:root {
--section-gap: clamp(5rem, 10vw, 10rem);
--leading-normal: 1.6; /* <- this one */
--text-sm: clamp(0.83rem, 0.8rem + 0.15vi, 0.88rem);
}
}The unitless value is what made it expensive rather than merely wrong. An absolute line-height inherits as one number: set 28px on the body and every descendant gets 28px. A unitless one inherits as a multiplier, so every descendant recomputes its own font-size times 1.6 and lands wherever that falls. A 12px label became 19.2. A 28.432px heading became 45.4912. Every element with its own size got its own leading, and every one of those leadings was a fresh fractional number.
That one declaration was all forty-four unsnapped leadings on the homepage. Fixing it took the count from ninety to eighteen.
It had also survived a gate I'd written specifically to catch this. The gate checks that every prose leading is a whole number of grid rows, and it passes, because I pointed it at an article page, and the prose there uses a different token. The rule I'd take from that isn't that the gate was bad. It's that a gate proves something about the page you aimed it at and nothing whatsoever about the page you didn't.
Two: the hairline tax
With the leading fixed, the remaining drift stopped being scattered and went suspiciously tidy. Plus one pixel, minus one pixel, minus two. Those three values accounted for 121 of the 154 nodes still off the grid.
A one pixel border is one pixel of layout. A section with 128px of padding top and bottom and a hairline across the top adds 257 pixels, not 256, and everything below it gets displaced by one pixel for the rest of the page. Twenty-one selectors on the homepage were doing exactly that, and every single one of them was a hairline. Not one was anything more interesting.
The fix is old and boring. A hairline pays for itself out of the padding on the same side.
article.exp-entry { padding-top: calc(var(--space-l) - 1px); }Written as calc against the same token the original used, so if the spacing changes the compensation follows it rather than becoming a stale magic number. Grouped in one block with a comment explaining the tax, rather than edited into twenty-one places where it would read as superstition to the next person.
I did think about moving the hairlines to inset box-shadows, which takes them out of layout entirely and needs no compensation at all, and I decided against it. The reason's worth stating. A box-shadow is invisible to anything that reasons about layout, this tool included. It would have fixed the number by hiding the thing the number is about.
Three: fluid type has fractional ascents
Eighteen down to thirteen came from the borders. The last few came from the type scale after all, though not in the way I first assumed.
The scale is fluid, so at 1280px it resolved to 14.08, 21.33, 28.43, 37.90, 50.51, 67.34 and 109.73 pixels. A font size isn't just a size. Ascent and descent scale with it, and the baseline inside a line box is half the leading plus the ascent, so a fractional size gives a fractional ascent gives a fractional phase, however exact your leading is. It also makes block heights fractional, which displaces everything below. The hero word measured 109.73 pixels tall and pushed the rest of the page a fraction out on its own.
You can't land a continuous function on a lattice, which is the same sentence as the one about fluid spacing, and I didn't expect to have to write it twice in one project. But there's a nicer answer here than giving up the fluidity, and it's the same function that snapped the leading.
--text-2xl: round(clamp(2.074rem, 1.6rem + 2.12vi, 3.157rem), 1px);Still fluid. It now moves in whole pixel steps instead of continuous ones, which is a difference no reader can perceive and the grid absolutely can. Nearest rather than up, unlike the leading, because a type size is a number somebody chose and leading is a number that only ever wants more air.
Where it stops, and why
Ten. Down from ninety, which moves this site from last in that table to fourth, past Tailwind, Atlassian, Salesforce, Ant Design and Carbon, and behind only the three that sit at five. On the 4px grid it went from 19.9% of text on grid to 30.7%.
Distinct drift values
- Before
- 90
- After
- 10
Text on a 4px grid
- Before
- 19.9%
- After
- 30.7%
Rank in the corpus
- Before
- 9th of 9
- After
- 4th of 9
| Measurement | Before | After |
|---|---|---|
| Distinct drift values | 90 | 10 |
| Text on a 4px grid | 19.9% | 30.7% |
| Rank in the corpus | 9th of 9 | 4th of 9 |
The last ten aren't going anywhere, and the reason's genuinely interesting. Several blocks measure exactly one pixel taller than their own line-height while having no padding, no border and no margin on them at all. That's the inline formatting context. A line box is as tall as the tallest inline box inside it, and a single child disagreeing by a fraction grows the line. CSS gives you no clean handle on that, and the fixes that exist are worse than the pixel.
Getting to five would mean removing type styles rather than correcting them, and that's a redesign wearing a bug report's clothing.
Which is the actual lesson, and it isn't a technical one. The three systems at five aren't there because somebody snapped their leading afterwards. They're there because a group of people agreed a small set of type styles before anything got built, and then kept saying no to more. Every one of my ninety was a decision that seemed reasonable in the section I was making at the time. That's the whole trick of it.
You can't retrofit that agreement. You can retrofit the arithmetic, which is what this was, and it's worth doing, and it took a media query hunt, twenty-one padding corrections and a rethink of the type scale to move one number. A design system isn't a token file, it's the constraint you accepted before you needed it.
Postscript: it is fourteen now
Within the day it went back up. The research card carries a live type specimen and quantising the scale had snapped that too, so the figure whose entire job is showing you the offset opened reading zero, sitting perfectly on the grid. A demonstration of a problem that opens by showing you no problem is a demonstration of nothing.
So the specimen now carries its own deliberately unsnapped leading and opens off the grid, which is the honest thing for it to do and costs four distinct drift values. Ten to fourteen. Text on the 4px grid went from 30.7% to 27.1%. Still fourth of nine, still ahead of Tailwind, Atlassian, Salesforce, Ant Design and Carbon.
I'd make the same trade again, and the number is republished rather than quietly held at ten. A measurement you only update when it improves isn't a measurement, and this whole piece is about a survey I published while coming last in it.