Skip to content

Sheet01 / 06 · Quoin

Versionv1.26.0

Sourcegithub.com/hawkbass/quoin

Consolequoin.check() on any page here

Side research · Quoin v1.26.0 · public, MIT, no dependencies

It puts a web page on a baseline grid

Print's had baseline grids since metal type, and in InDesign it's a checkbox you tick once and never think about again. The web has never had one at all, because line-height centres text inside its box instead of seating it on a line, so the baseline ends up wherever a typeface's asymmetry puts it. The first version of this corrected a page that had already rendered, and that holds right up until a media query moves a padding by thirteen pixels and every correction is wrong at once. So it fits the design instead, before anybody has written the CSS, and it does that without changing a single one of the sizes you asked for. Open the console anywhere on this site and run quoin.seat() to watch the older half of it work.

  • 112Of 134, seated
  • 0Sizes a fit has to change
  • 76msTo fit, with no browser
  • 4th of 9Coherence vs design systems

Nobody asked me for it. Print has had this since metal type and the web has never had one, which is most of what you need to know.

Sheet02 / 06 · What it does

Main pathquoin fit · before the CSS exists

Older halfcheck, seat, css · in the console

Needstext-box-trim · Baseline since Aug 2026

02 · What it does, and what stopped it working the first three times

A correction is a repair. Fit the design and there's nothing to repair.

A correction is a repair on something already wrong, and it only ever describes the one layout you measured it against. Fit the design first and there's nothing left to repair, at any width.

Seating this site, three routes

The homepage ends with 112 of its 134 text nodes on the grid, the case study reaches 149 of 152, and the writing index reaches 54 of 57. Nothing is excluded from those counts, headlines included, even though the tool declines to snap display type by design, so the readings hold it to a job it says up front it won't do. It doesn't say what any of them started with, and that's deliberate: the starting count came back 29, then 30, then 41 across three runs of the same page, because it moves every time a word on the page moves. A number that unstable has no business being printed to the unit on a site that argues you should be able to reproduce the ones it prints. Every one of these is gated by e2e/seating.spec.ts, which fails the build the day correction stops working, and none of them is typed by hand: they're read from what the test measured.

Where a correction stops, on this site, measurably

The homepage used to reach 97%, and the redesign took it down to 84%. Nothing about that is a thing breaking. Every section on the new design is a full viewport tall with its content centred, and a centred column is the one arrangement a correction can't reach. Padding a block makes the block taller. A taller block makes the centred stack taller, and a taller stack moves its own centre up by half of what you've just added. So the correction chases its own effect. You can watch it settle: the sweep converges after five passes and sits in the same place at thirty. What's left drifts in matched pairs of opposite sign, which is what a fixed point looks like from outside. The writing index isn't centred and it still reaches 95%. I could have kept the old number by moving the sections. I'd rather have the reading, because this is the argument the tool now makes for itself. A correction only ever describes the page you measured it against, and on a centred layout it can't fully describe even that one. Fit the design first and the question never comes up.

What a correction cannot survive

Corrections describe one arrangement of line breaks, so I measured how far that actually carries. A page seated at 1280 and taken down to 375 kept all of it when only the line breaks had moved, because snapping every leading to whole rows means the page reflows in whole rows. The same page collapsed to nothing the moment a media query changed a container's padding by thirteen pixels. A hairline border survives, an image with an odd height survives, a font-size change at a breakpoint mostly survives. A padding change does not, and every real site has one, which is what told me corrections were never the right primitive.

The arithmetic that makes the sizes free

Trim the boxes with text-box-trim and a block starts at its cap height and ends at its baseline, so the distance from one baseline to the next comes out as the line count minus one, times the leading, plus the space above the next block, plus its cap. The line count is the only term the viewport can touch, and it's multiplied by a leading that is already whole rows, so modulo the pitch it contributes nothing whatsoever. What's left is that space plus cap has to come to a whole number of rows, and every term in that belongs to one block on its own. Nothing ties one size to another, which is the whole reason no size has to move.

Why correcting a page isn't correcting an element

Seating a block shifts every block underneath it, so you can't batch the corrections, and in a flex row aligned to the end it shifts the blocks above it as well, and that means seating the third one lifts two you'd already finished and puts them back out. So it sweeps until nothing moves, which on this site takes three passes.

Fitting a design nobody has built yet

It takes a design as JSON, or reads one off a live page when there's a site already, or takes Figma's own node JSON straight from the REST API without being told that's what it is. Hand it a font file for each family and it needs no browser at any point, because the cap heights come out of the OS/2 table in about 76 milliseconds, and that's the difference between something you run in a build and something you run in a review. Hidden layers get skipped on the way in, because a hidden layer is a design somebody has already rejected and fitting the page to include it is fitting it to a decision that's been taken.

It doesn't trust its own corrections

It measures again after every correction, and escalates to a second lever when the text hasn't landed, and calls it a miss when neither lever could move the thing. There is no optimism anywhere in it, and a corrector that assumes it worked is just the verifier with extra steps.

Sideways, where the problem does not exist

Japanese, Chinese and Korean have been set vertically for most of their history, so I turned the tool ninety degrees to see whether the same correction applied. It doesn't, and the reason is better than the answer: in vertical writing the dominant baseline is the central one, and central means centred, so the ascent that this whole library exists to correct is simply absent from the sum. What's left is arithmetic on numbers the designer already wrote down. Every leading a whole number of rows, every leading the same parity, every space a whole number of rows. No cap height, no font table, no browser. It fits and it checks, and the engine that can't run the horizontal method at all because it lacks text-box-trim runs this one perfectly.

Except for ruby, which hands the font back

Furigana is ordinary in Japanese setting, and a tool that falls over on it isn't fitting Japanese. An annotation is a second run of type beside the base text, and if the leading can't hold both, the engine reserves the difference at the leading edge and every block below it moves. Nothing in CSS fixes that. Enough leading does, and the threshold is the font's own ascent plus descent times the size plus twice the annotation, which held at fifty-eight of sixty held-out combinations. Note what that contains. The vertical baseline is font-free and stays font-free, but an annotation is a box rather than a baseline, and a box is font-sized, so ruby puts back exactly the dependence that vertical writing takes away. Given the annotation without the metric, the fitter says so and leaves the leading alone.

Why you can't just do it in CSS

This site quantised its entire scale to find out. Fixed spacing, every leading snapped with round(), that gets you rhythm and it stops dead at 27.1%. Sweeping the grid origin across every sub-pixel position peaks at 29%, because every font at every size starts at its own phase and no single origin suits the lot of them.

Sheet03 / 06 · Why not CSS

Ceiling27.1% quantised · 29% sweeping the origin

GroundThe only accent surface on this page

Reproducenpm run corpus

03 · Why you can't just do it in CSS

27.1%

Where quantising the whole scale in pure CSS stops. Fixed spacing, every leading snapped with round(), and it gets you rhythm and nothing else.

Sweeping the grid origin across every sub-pixel position peaks at 29%, because every font at every size starts at its own phase and no single origin suits the lot of them. CSS can give a page its rhythm. Only measurement could give it phase, until text-box-trim reached its third engine and the arithmetic came apart instead.

Sheet04 / 06 · The corpus

MethodHomepages at 1280px, half-pixel tolerance

BandingUnder 25 · 25 to 59 · 60 and over

Reproducenpm run corpus

04 · Nobody else has one either

The last column is the one that matters.

Percentages say how far off a grid a page is. Distinct drift values say whether the type scale and the spacing scale agree with each other, which is the thing you can actually fix.

Homepages at 1280×900, 0.5px tolerance, grid origin solved from each page, not pinned to zero. Measured 2026-08-29 with quoin 1.22.0. Design systems from the Quoin corpus. 13 of 35 rendered enough text to characterise; the rest are dropped rather than scored, because a page with forty text nodes gives a percentage that means nothing.
Design systemNodesOn a 4px gridOn an 8px gridDistinct drifts
Base Web14086.4%44.3%19
Atlassian Design9563.2%38.9%18
Ant Design9645.8%35.4%24
Tailwind CSS13744.5%33.6%27
Shopify13743.8%32.8%49
Bootstrap8944.9%32.6%41
Material Design 311857.6%29.7%9
Nord Health6149.2%29.5%35
Apple Human Interface Guidelines6144.3%29.5%24
Chakra UI8246.3%29.3%19
Open Props167248.6%27.9%188
Bulma15543.9%26.5%61
Mantine20633.5%17.5%121

Sheet05 / 06 · The honest limits

Count6, read from the list

Why hereA tool that hides these gets uninstalled

StatusPublic, MIT, no dependencies, 28 kB. On npm when I think it's ready, and not before.

05 · The honest limits

6 things it will not do.

  • It won't pretend it moved something it didn't. Padding can't shift a flex child that its parent centres, and it can't shift an inline box either, so every correction gets re-measured and escalated to a second lever, and if neither one worked it gets reported as missed rather than quietly counted.
  • It won't tell you to ship the JavaScript. Correcting layout at runtime means the reader sits and watches the text jump about, so it hands you the CSS and fully expects you to delete the script.
  • It won't snap your headlines. Body text is the job, because a headline is a shape rather than a line of reading, and forcing one onto an 8px rhythm just makes it worse, so display type opts out by selector.
  • It won't fail on half a pixel. Sub-pixel layout lands baselines a fraction off constantly, and a tool that goes red on 0.02px is uninstalled by Tuesday.
  • It won't take an estimated cap height. There is deliberately nowhere to put one, because a cap height guessed to within five per cent is a baseline out by half a row, and a fit built on a guess is worse than no fit at all because it looks like it worked.
  • It won't claim a fit on an engine that can't hold one. text-box-trim only reached its third browser in August 2026, and on anything older than that it reports itself unavailable rather than handing you numbers that quietly don't apply.

Sheet06 / 06 · Prelight, shelved at v0.3.0

Tagged18 April 2026

Versionv0.3.0

Sourcegithub.com/hawkbass/prelight

06 · Before this one · Shelved at v0.3.0

Prelight: a layout linter for translated ui

Translators turn Save into Speichern, CI runs in English, and the overflow ships. Prelight checks every string at every font scale at every slot width in single-digit milliseconds without launching a browser at all, and it works: 440 tests, a 928-case corpus, and better than 98% agreement with all three engines.

It solves a problem that's already got workarounds, because you can draw the button wider or get a translator to look at it or cap the string in the CMS. Prelight does it properly and it does it earlier, and what it saves you is an embarrassment rather than a rewrite. That's a real thing to be worth, and it's also a ceiling.

Quoin hasn't got a ceiling I've found yet, because the web has never once put a page on a baseline grid, print's been doing it since metal type, and not one of the nine reference design systems I measured has managed it either. One of these is a better linter than the alternatives, the other is a thing that didn't exist.

So it's paused rather than dead. It's tagged and public and MIT and it still passes, and if the translated-overflow problem turns out to bite harder than I reckon, I know exactly where I left it.

Prelight on GitHub

Public, MIT, tagged and still passing. Parked because I found a better problem, not because it stopped working.