← Back
[W] Writing

The invisible box around text

You give a button the same padding on top and bottom. The label still doesn't look centred. So you add a pixel to the top, and it looks right, until someone changes the font.

The padding was never the problem. It's measured from a box you can't see, and that box doesn't fit the letters.

Try it below. Switch fonts, then turn on the trim.

The lines come from the font's own numbers, read live in your browser. The red bands are half-leading, extra space that line-height adds. The readout measures the space above the capitals and below the baseline.

What's in the box

When you set font-size: 16px, you set the size of the em, an imaginary square the letters are designed in. A font's glyphs are drawn on a grid inside it, often 1,000 or 2,048 units tall. But nothing says the letters have to fill it, or stay inside it.

The font also declares two numbers for layout. The ascent is how far above the baseline it needs room for, and the descent is how far below. Together they make the content area, the box the browser actually lays out.

Those numbers have to cover every glyph the font has: an É with its accent, a Å with its ring, the tail of a g or a y. So the box is taller than the letters in most words. Measured in Chrome on macOS, Arial's ascent is 0.91em and its descent is 0.21em. That's a box 1.12em tall around letters whose capitals are 0.72em.

ascent cap height x-height baseline descent half-leading half-leading
The ascent leaves room for the accent on É, and the descent for the tail of p. A word like "Save" only uses the band between the red lines. Look closely at the p: its bowl pokes slightly past the lines, the same overshoot every round letter has.

Two more lines matter. The cap height is where flat capitals like H stop, and the x-height is where lowercase letters like x stop. A label like "Save" lives almost entirely between the baseline and the cap height. It never uses the room for accents above or descenders below.

One font, three sets of numbers

It gets messier. For historical reasons, a font file stores its ascent and descent three times.

There's the hhea table, from Apple's original TrueType format. There's a "typo" set in the OS/2 table, meant for typographic line spacing. And there's a "win" set in the same table, which Windows used to decide where to clip glyphs. Which one gets used depends on the operating system and on a flag in the font. Google Fonts' vertical metrics guide sums it up: macOS uses hhea, Windows uses typo only when the font sets the USE_TYPO_METRICS flag, and otherwise uses win.

So if a font's three sets disagree, the same button can sit differently on a Mac and on a Windows PC. Well-made fonts make them match, and the OpenType spec now asks variable fonts to set the flag and keep hhea equal to the typo values. You can't fix this in CSS, but it's worth knowing when a label looks fine on your machine and off on someone else's.

Where line-height puts the rest

Then line-height adds its own space. CSS works out the difference between the line height and the content area, and splits it equally above and below. Each half is called half-leading.

Dashed lines mark each line box. The grey fill is the content area, ascent to descent, and the red bands are the half-leading. Drag below about 1.2 and the half-leading goes negative.
const contentArea = (ascent + descent) * fontSize;
const halfLeading = (lineHeight - contentArea) / 2; // above and below

Equal above and below sounds fair. But it's equal around the content area, not around the letters. If the font's ascent leaves more room above the capitals than its descent leaves below the baseline, the capitals sit low by half the difference, at any line-height and any padding.

// How far the capitals sit below centre, in em
const offset = (ascent - capHeight - descent) / 2;

In Arial that's almost exactly zero, so it centres well. Using the numbers measured above, the system font on a Mac comes out around 0.03em low and Verdana around 0.04em. At 16px that's about half a pixel. On a 44px label it's more than a pixel, and fonts that reserve extra room above for stacked accents can be off by much more.

There's a second effect. Your eye centres the word, not the font. Lowercase letters only reach the x-height, so a word like "save" has most of its ink lower down than "SAVE" does. That's partly a judgement call, so the usual convention is to centre the band from the baseline to the cap height.

Trimming the box

CSS can now shrink the box to the letters. text-box-trim says which edges to trim, and text-box-edge says where to trim them to. The text-box shorthand sets both.

.button {
  padding-block: 12px;
  text-box: trim-both cap alphabetic;
}

cap trims the top of the first line to the cap height, and alphabetic trims the bottom of the last line to the baseline. Now the label's box is exactly the band "Save" uses, so equal padding looks equal. It's what Figma's vertical trim setting does to a text layer.

As of November 2026, it's supported in Chrome and Edge 133, Safari 18.2 and Firefox 154. MDN lists it as Baseline from August 2026. Older browsers ignore it and you get the untrimmed box, so it's safe to add today. It works on block containers everywhere, but trimming inline elements only works in Firefox, and partly in Safari.

Before this, the fix was Capsize. It reads the font's metrics and generates negative margins that pull the box in to the cap height and baseline. It's what the demo falls back to when your browser can't trim. It works, but the numbers belong to one font. If the font fails to load and the fallback draws instead, the margins are wrong.

The worst fix is the one most of us have shipped: nudging the padding by a pixel until it looks right. It's tuned to one font, at one size, on one platform.

Same font-size, different sizes

The em also explains a related surprise. Set two fonts to the same font-size and one can look much bigger.

font-size sets the em, not the letters. Each font decides how big its letters are inside it. Measured in Chrome on macOS, Verdana's x-height is 0.55em and Times New Roman's is 0.45em. So at the same font-size, Verdana's lowercase letters are about 22% taller.

That matters most when a web font falls back to a system font, or when you mix two families in one line. font-size-adjust fixes it by choosing the x-height instead of the em.

body {
  font-family: "Brand Sans", Verdana, sans-serif;
  font-size-adjust: from-font; /* keep the first font's x-height */
}

A number like 0.5 means "scale each font so its x-height is half the font-size". from-font takes that number from the first font in the list, so the fallback matches it. It's supported in every major browser, with Chrome the last to add it, in version 127.

All of these come from the same idea. The box CSS gives you was built to hold any text. Your label is one particular word, so if you want it to look centred, measure what the word actually uses.

Next