Quoin puts a web page on a baseline grid. It measures where the first baseline lands inside every block on a rendered page, works out how far each one sits from an eight pixel rhythm, and writes the corrections out as a stylesheet you can ship without the JavaScript. Its own site is built that way, and the last step of the build is the tool running against the page it just assembled. If the page doesn't seat, the build fails.
A layout changes with the width, so the build measures at six widths and wraps each set of corrections in a media query for the range it belongs to. I'd already learned that one the hard way: seating the page once at 1280 gave me a hundred per cent at 1280 and 79% at 820, because a definition list that sits on one line at the wider size wraps to two at the narrower one and everything underneath it moves.
So I redesigned the site at the end of August. A print forme, the four inks across the top, a docket strip carrying the pitch and the live score, six plates below it. The build seated it, reported a hundred per cent at all six breakpoints, and I pushed it.
375 and 380
A few days later I was checking the thing on a phone and read the score in the page's own header, which is measured live in the browser rather than baked in at build time.
2% on the 8px grid · 4 of 179 blocks
Two per cent on the grid. Four blocks out of a hundred and seventy nine, on the site for the tool whose entire job is that number.
The build measures the narrow range at 380 pixels. I was looking at 375. Five pixels of difference between a page that's perfectly seated and a page that's barely seated at all, and I'd shipped it a week earlier without ever putting it in front of a phone.
So I swept it properly, in the same headless browser the build uses, at every width I could think of that somebody might actually be holding.
360
- Seated
- 2.7%
- Sampled by the build
- no
375
- Seated
- 2.2%
- Sampled by the build
- no
380
- Seated
- 100%
- Sampled by the build
- yes
390
- Seated
- 11.5%
- Sampled by the build
- no
393
- Seated
- 11.5%
- Sampled by the build
- no
412
- Seated
- 11.5%
- Sampled by the build
- no
430
- Seated
- 11.5%
- Sampled by the build
- no
| Width | Seated | Sampled by the build |
|---|---|---|
| 360 | 2.7% | no |
| 375 | 2.2% | no |
| 380 | 100% | yes |
| 390 | 11.5% | no |
| 393 | 11.5% | no |
| 412 | 11.5% | no |
| 430 | 11.5% | no |
Every width the build sampled came back at a hundred. Everything between them came back in single figures. The six numbers in the build log were all true and the page was still wrong for everybody, which is the shape of defect I keep finding in my own work and keep being surprised by.
The control I should have run first
The obvious suspicion is that this is simply what the tool does. Per-range seating would then be a sampled answer wearing the clothes of a continuous one, and I'd have been publishing a limitation without knowing it. That's worth knowing either way, so I went and checked rather than assuming.
The site had an older design, still sitting in git, written months before any of this. I checked it out, built it with the same tool and the same six ranges, and ran the identical sweep against it.
A hundred per cent at every width. Sampled or not. Not one of them below.
So it wasn't the tool. It was the page I'd just designed, and the difference between the two stylesheets is the whole of the rest of this.
What a correction is, and when it survives
A phase correction is padding on the top of a block, sized so the first baseline inside that block lands on a grid line. It's computed against one rendering, at one width, and then written down as a number of pixels.
It carries to a different width on exactly one condition. Every box above it has to be a whole number of grid rows tall. If that holds, then a paragraph reflowing from four lines to three moves everything under it by exactly one row, the grid repeats every row, and every correction below it still lands where it did. Break the condition anywhere and everything past the break is out by whatever fraction you broke it with.
That condition is rhythm, and rhythm is the half of this that CSS has always been able to do. The old design held it on purpose. There's a comment in its stylesheet about six section borders and seventeen table rows adding up to twenty three pixels of accumulated offset, and every bordered box in that file subtracts its own border back out of its own padding.
The new one didn't hold it anywhere. 213 boxes out of 345 weren't a whole number of rows tall. Ninety six vertical values that weren't multiples of eight, and thirty six borders that nobody had taken back out of a padding. It looked immaculate and it was arithmetically all over the place.
Four rules, and all four of them are CSS
Every leading is a whole number of rows
Nine blocks were set at 20px leading. Three lines of that is 60 pixels, which isn't a multiple of eight, so a paragraph wrapping to three lines instead of two shifts everything below it by four. Two more were set as a ratio of 1.47, which at 17px gives you 24.99, and a hundredth of a pixel is still not a grid row.
This is the easy one and it's the one everybody already half does. Pick the pitch, then only ever use multiples of it for anything vertical.
Fluid type needs a snapped leading
The masthead's font-size is a calc against the container width, so it changes continuously as the window moves. A ratio leading on top of that is continuous as well, which means a two line masthead whose height slides by fractions of a pixel drags the entire page along with it. There's no width at which it's wrong and no width at which it's right.
/* Continuous. Cannot be on a grid at any width. */
line-height: .95;
/* Discrete. The type stays fluid, the leading steps in whole rows. */
line-height: round(.95em, 8px);round() snaps the computed leading to the nearest whole row, so the height steps by eight pixels rather than sliding through every value in between. It's been in every engine since 2023, it's in Quoin's own README as the recommended way to fix a leading, and I'd still never reached for it in anger.
A border is in the vertical flow
A table cell with 24px of leading, three and a half pixels of padding above, four below and a one pixel bottom border is 32.5 pixels tall. Do that seventeen times down a table and the page under it is eight and a half pixels out, with nothing in the stylesheet looking wrong.
The rule is that every box takes its own border back out of its own padding on that side. It's tedious and it's mechanical and it's the single highest yield thing in this list, because borders are everywhere and a hairline is the easiest pixel in the world to forget you spent.
The stacked layout is where it caught me twice. Twelve boxes that carry a border on the right at desktop swap it for a border on the bottom when the columns stack, and a border on the right costs the vertical flow nothing while a border on the bottom costs it a pixel. The desktop layout was fine and the phone layout was out by twelve.
An inline run at a smaller size is taller than the line holding it
An inline element at a different font-size sits its baseline differently inside its own box, because the half-leading is the line-height minus the content height over two, and a smaller font has a smaller content height. Align the two baselines and the line box becomes the union of both boxes, which is taller than the line-height you asked for.
One pixel per wrapped line. That's how a five row table with a code snippet in the first column came out 41 pixels tall against a 16 pixel leading, and how eighteen of nineteen paragraphs on the old site went off the rhythm before I found it there too.
/* A zero contributes no box of its own, so the parent's strut
governs the line and the glyphs draw exactly where they did. */
code, kbd, samp { line-height: 0; }What it moved
Every box on the page is now a whole number of rows tall at every width I've measured, and the sweep that was in single figures looks like this instead.
320
- Before
- 7.7%
- After
- 100%
360
- Before
- 2.7%
- After
- 99.4%
375
- Before
- 2.2%
- After
- 100%
390
- Before
- 11.5%
- After
- 100%
430
- Before
- 11.5%
- After
- 100%
620
- Before
- 12.1%
- After
- 100%
1024
- Before
- 12.0%
- After
- 100%
1280
- Before
- 27.8%
- After
- 27.4%
1600
- Before
- 13.7%
- After
- 100%
| Width | Before | After |
|---|---|---|
| 320 | 7.7% | 100% |
| 360 | 2.7% | 99.4% |
| 375 | 2.2% | 100% |
| 390 | 11.5% | 100% |
| 430 | 11.5% | 100% |
| 620 | 12.1% | 100% |
| 1024 | 12.0% | 100% |
| 1280 | 27.8% | 27.4% |
| 1600 | 13.7% | 100% |
1280 is the one I haven't fixed, and it's honest to say why rather than leave it out of the table. It's the tool rather than the stylesheet. A correction is padding, padding changes the height of the box it's correcting, and in a grid row the row's height is whichever item in it is tallest. So a fractional correction on one cell moves the row, and moving the row moves everything under it.
The tool already carries the lever that fixes this. It can position a block relative and offset it with top, which moves what gets drawn without changing what gets laid out, and it's in there as a fallback for when padding fails to move a baseline at all. Preferring it for grid and flex children is the next change, and it wants its own tests rather than being smuggled in at the end of this one. 166 of 179 blocks at 1280 are inside a pixel in the meantime.
The one that nearly caught me on the way out
While I was in there I checked the touch targets, because my portfolio puts forty seven controls on a phone and not one of them is under 44 pixels, and this page had nine controls with eight of them under and three at thirteen pixels. So I added a pointer: coarse block that padded them out, measured it, and got 44 or better across the board.
Then I ran the seating sweep again with a real touch profile rather than a narrow window, and the page scored 7.3% at every width while the build still said a hundred.
The build seats with a mouse, because a headless browser has a mouse. Every correction it had written was computed against a layout that no phone anywhere renders, and I'd introduced that by fixing an accessibility problem. The rule was right and the mechanism was wrong: anything keyed to the pointer has to leave the geometry alone, or the whole build needs to know about it.
The hit areas are extended with an absolutely positioned pseudo-element now. The controls sit side by side, so a 48 pixel box centred on each one reaches past it vertically and overlaps nothing, and the layout is identical on every pointer. Touch and mouse seat to the same number at all fourteen widths I checked.
Rhythm is CSS. Phase isn't.
That's the division, and it's worth being precise about because I've spent a year on the wrong side of it. Every vertical distance a whole number of rows is something CSS gives you outright, and it's the part that makes a correction portable. Where the first baseline lands inside its own box is a function of the ascent, which lives in a table inside a font file, and CSS gives you nothing for it at all.
So write the rhythm into the stylesheet and let a tool do the phase. Get the rhythm right and the phase corrections hold at every width, because each one then depends only on its own block's type rather than on the accumulated arithmetic of everything above it. Get it wrong and no amount of correction generalises, because you're correcting a layout instead of a system, and a layout is only ever the layout at one width.
The measurements are reproducible. The site is at quoin.craighawkes.dev and it reports its own score live in the header, on whatever width you happen to be reading it at. If it ever says anything other than a hundred, that's the bug and it's mine.