"A text line box is the taller of the declared line-height and the font's natural box"
▸ SYMPTOM
You build a UI panel to a browser mockup (a modal, a list, a stack of buttons) and it looks right for a few rows. Then you total a column of ten rows and find it runs 70px past the bottom of the region you sized for it. Nothing in the box model looks wrong: padding, borders, margins, gaps and explicit width/height all measure exactly what you declared. The overflow only shows up once the stack is tall enough to expose the accumulated slack.
▸ CAUSE
A text element's line box measures max(declared line-height, the font's own natural box). Measured on a live 1:1 capture (a ScreenPanel pinned to Scale = 1, so one CSS px is one image px):
font-size: 16px; line-height: 22pxrenders a 33px line box.font-size: 13px; line-height: 20pxrenders a 30px line box.
Only the line box inflates (every other box-model value measures exactly as declared), which is exactly why the drift hides. A single row looks fine; the error is per-row and only becomes visible when you sum a column.
Corrected mechanism, 2026-08-01 (engine 26.07.22, live measurement of seven elements via a dev ConCmd reading Panel.Box.Rect): the "1.5× the declared line-height" reading of the two data points above is a symptom, not the law, and the real mechanism changes what it is safe to predict. The natural box runs about 0.95× font-size for a body face and about 1.08× font-size for a Poppins-weight display face. Across the seven elements the ratio to the declared line-height ranged all over the place, 1.00 to 1.57, while the ratio to the font size held steady within each face. The one element in the set that declared a line-height above its natural box (a 23px font declaring 30px) measured exactly 30.0, a 1.00 ratio, proving the floor is the font's natural box and not a fixed multiplier of whatever is declared.
So: declaring a line-height below the font size does nothing, the natural box wins regardless. There is no way to shrink a line box below its glyphs by lowering the declared number, only by lowering the font size.
The consequences compound through anything sized from a mockup's row arithmetic:
- A 13px button with 9px vertical padding and a 1px border stands 50 tall, not 40.
- A list row with 9px padding stands 48, not 38.
- A modal body sized from a mockup's row totals needs its fixed scroll regions shrunk by the accumulated difference, or its outer size grown.
▸ FIX
Size rows off the font size, not off an assumed 1.5×, and reserve an explicit line-height declaration for asking the engine for more leading than the glyph needs. Declaring smaller line-height values does not shrink anything below the natural box, and it still breaks the design system's type scale for every other panel that shares it. Size honestly around the real line box:
- Derive every fixed sub-height from the ONE column with the least slack, and write the arithmetic in a comment next to the number so the next edit keeps the budget.
- Verify the result on a scale-1 capture, not by construction. Pin the panel to
AutoScreenScale = false, Scale = 1so one CSS px maps to one image pixel, then measure the rendered rows with a ruler rather than trusting the mockup's math.
▸ WHY IT WORKS
The inflation lives in the line box, not in padding, borders, or margins, so the box model you can see is telling the truth and the type metrics are the hidden variable. The font's natural box acts as a floor the declared value cannot go under, which is why a declared line-height that sits below it has no effect at all. Once you measure the real line-box height on a 1:1 capture and budget fixed regions from the tightest column, the accumulated per-row slack has nowhere to hide, and the stack lands inside its parent instead of spilling past it. Leaving the type scale alone keeps every other panel that inherits it correct.
- Corrected mechanism: the line box is max(declared line-height, the font's natural box), not a fixed 1.5x multiplier of whatever is declared.
- Published