dev/

The Chaotic Neutral Nature of Font-size

The first step to properly aligning web typography is understanding font⁠-⁠size


CSSTypography

An illustration showing the difference between font-size and the height of uppercase and lowercase letters

font-size is mystery meat in vegan times.

If you’re anything like me, you have picked a font size in a text editor or a design app, or set it in a stylesheet—and wondered what on the screen was supposed to be the size you chose. Neither the uppercase letters—nor the lowercase ones—matched your pick.

If you’ve ever gotten a flattened design comp and had to figure out the font sizes used—you also know the pain of having to reverse engineer type sizes: “This uppercase type measures 21px - so the font-size must be 28px? No… 26px? 27px??”

You may have changed the font family and noticed how the type size changed, without you even touching font size.

You may also have shrugged your shoulders and thought: “I guess that’s just how it is.”

I know I did.

But why do we have such a weirdly indirect way of setting type size?

Why is it so inconsistent?

And—can we do anything about it?

Origins

Cast types assembled to form lines of text with leading blocks to control line height

A basic illustration of movable type showing cast types assembled to form lines of text.

Is this the source of font-size?

What does font-size even mean? Where does it come from?

To find out what defines font-size in web typography—the em unit seems like a good place to start. It relates directly to font-size; so 1em is equal to the font-size value set on an element.

The unit name comes from em square—which in pre-digital typography was based on the width of the uppercase “M”. But—that doesn’t seem to have any practical footing in reality in today’s digital typography.

A better explanation may be found if we go back to 1440 when Johannes Gutenberg invented the printing press. It used movable type; individual cast metal types with letters on them, that could be assembled to print a full page in one go. A full set of those metal types for a typeface was called a font—and hence font-size (or em) would refer to the height (i.e. size) of those metal blocks—and not the actual letters.

So the thing we are setting the size of, is a (now) virtual metal block, that is displayed exactly nowhere on screen? Not super useful. Thanks, Obama Johannes.

Whether this is the actual explanation or not—one thing seems factual: font-size is a measure that is not physically on screen.

Challenges

It’s also evident that the relationship between font and letter size varies considerably across fonts. It almost feels like fonts are divas—preferring to not share the stage with other fonts.

And that raises several problematic issues.

Swapping fonts

An illustration showing text set in the Titillium Web font
An illustration showing text set in the Futura font

Switching between two different sans-serif fonts without changing font-size can yield wildly different letter heights.

In this example, the former is only 90% of the latter.

Before: Titillium Web Bold.

After: Futura Bold.

If you’re switching out one font for another, it may require adjustments of font-size (and also line-height if you use relative values) to keep visual density consistent. A tedious process if you’re trying out different fonts to find the perfect one.

A note on font-weight

You may also have noticed that font-weight can vary unpredictably from font to font; even though there’s a more or less standardized range from 100 to 900—the values do not map to actual stem thickness consistently. What is 400 in one font does not necessarily match what is 400 in another. Add to that that each font may also support only parts of the range, often leaving out the upper and/or lower ends of the spectrum.

Pairing fonts

An illustration showing two fonts with different sizes
An illustration showing two fonts scaled to the same size

A pairing of a serif font (Libre Caslon Text) with a sans-serif font (Titillium Web).

Before: At the same font-size the former is larger than the latter — and so the top of the letters do not align.

After: Alignment is achieved by reducing the font-size of the serif font to 88% of the sans-serif.

Combining two different fonts on the same line can be a chore too; you’ll have to adjust the font-size of one or the other to get them to match. Finding the correct ratios by trial and error is cumbersome and can be difficult across type scales; what looked right in smaller sizes might not match in larger—and vice versa.

Inline alignment with fonts

Before aligning text with inline elements (e.g. icons, checkboxes, radio buttons, etc) you need to identify what type of alignment fits the elements: do you align to the uppercase or lowercase height of the text?

An illustration showing uppercase text unaligned with icons
An illustration showing uppercase text aligned with icons

Before: The closest whole number value for font-size doesn’t visibly align the text with the icon.

After: Text size is adjusted by 5% to align perfectly.

If the elements have a common prominent upper and lower boundary—then aligning them to the baseline and the uppercase height of the text is a good fit.

An illustration showing lowercase text unaligned with icons
An illustration showing lowercase text aligned with icons

Before: Lowercase letter height is slightly off.

After: Perfectly aligned with a small font-size adjustment.

If they have a variable vertical boundary—you may want to align the visual center of gravity of the elements and the text. So center to the height of the most predominant case of the text.

Accessibility

The most problematic of all is the handwavy nature of font-size. It makes accessibility recommendations seem futile. A user may well have a preference for 30px body text, but since every font yields a different size for 30px, that just doesn’t make any sense.

Solutions

Fortunately, the CSSWG has come up with a simple solution: the font-size-adjust property.

Initially, it was drafted with a single-value syntax where the value would automatically scale the ex-height (i.e. lowercase letters). But the newest version has a two-value syntax that adds the ability to define which metric should be scaled, e.g. cap-height for uppercase letters.

To conservatively set the height of the type to something in the ballpark of existing ratios—but leveled out across fonts:

:root {
	font-size-adjust: cap-height 0.7;
}

You will still need to do a little math to know what you’re getting, e.g. if you set a font-size of 30px you will get 21px tall uppercase characters (30px * 0.7). But at least you get consistency.

Or—you can be cheeky and set the type size to match font-size exactly (no more math needed!):

:root {
	font-size-adjust: cap-height 1;
}

This would be how I would have expected font-size to work without prior knowledge: set font-size: 1rem;—get 1rem type.

But…

Support

Unfortunately—at the time of writing—support is not that great overall:

  • Chrome 43 and Edge 79 shipped single-value syntax behind experimental feature flags—with no mention of two-value syntax anywhere.
  • Safari only got single-value support from version 16.4—and Safari 17 added two-value syntax support.
  • But Firefox has everyone beat with support for the two-value syntax since version 3(?!).

So—it looks like we will have to dig deeper still.

Legacy support

An illustration of font metrics

The basic font metrics needed for calculating the actual type size.

Note the virtual font block shown for “H”; the actual measure of font-size.

Luckily we can mimic font-size-adjust by leveraging the same information that the browser makes use of: font metrics.

Having dabbled in making my own fonts and bundling icons into fonts—I was aware of font metrics, but I just assumed that they were like ingredients that go into a cake; you can’t get them out again.

So my mind was naturally blown when I read Vincent De Oliveira’s brilliant Deep dive CSS: font metrics, line-height and vertical-align blogpost.

He describes how you can dig out font metrics (e.g. cap-height and x-height) from font files, using tools like the opentype.js Font Inspector, and use the numbers to calculate stuff such as actual letter height.

The metrics we need are:

  • unitsPerEm: This is the root metric—all other metrics relate to this. An arbitrary number, set by the font designer. Found in the head table of the font.
  • sxHeight: The x-height. Located in the OS/2 table.
  • sCapHeight: The cap-height. Also located in the OS/2 table.

E.g. for Roboto Mono we get unitsPerEm of 2048, sxHeight of 1082, and sCapHeight of 1456.

By dividing sxHeight and sCapHeight by unitsPerEm we get ratios just like the numbers used in font-size-adjust: x-height of 0.52832 and cap-height of 0.71094.

h1 {
	font-family: "Roboto Mono";
	font-size: 2rem;
	font-size-adjust: cap-height 1;
	line-height: 1.5em;
	--cap-height: 0.71094;

	@supports not (font-size-adjust: cap-height 1) {
		font-size: calc(2rem / var(--cap-height));
		line-height: calc(1.5em * var(--cap-height));
	}
}

You could implement a solution like the above example using font-size-adjust for browsers supporting it—and a fallback for the ones that don’t.

Note: Any use of em units would have to be compensated in the fallback solution as font-size is changed—as in the case of line-height.

Demo

Font metrics
Units per em 1000
Cap-height 718
X-height 538
CSS vars
 
--cap-height 0.718
--x-height 0.538
Hx
6rem 6rem

To help show off how type can be scaled via font metrics (using fontkit) check out the nearby figure:

  • Em: Shows the font without scaling.
  • Cap-height: Shows how the font is scaled for the uppercase letters to match the font-size.
  • X-height: Displays how the type size is changed to match lowercase letters to font-size.

Out of the box you can select between General Sans and Roboto Mono and inspect their height-related font metrics.

But you can also drag and drop a font file onto the figure to test it out - and get its cap-height and x-height font metrics. WOFF, WOFF2, TTF, or OTF formats should work—variable or otherwise.

And lastly, you can copy the CSS variables directly from the table to test them in your own code.

Disclaimer: Some fonts can have wrong or even missing values for cap-height and x-height causing the chart to not align the different horizontal lines with the letters. So if you encounter this—a kind bug report to the font designer might be in order.

Beware of inconsistent variable fonts

An illustration showing the height differences across font-weights in the Titillium Web font

In Titillium Web cap-height changes with font-weight (and to a much lesser degree also x-height)—shown here by the two extreme weights: Black and ExtraLight.

If you are using variable fonts, be aware that a fraction of fonts change letter height across font weights. E.g. Titillium Web’s lighter font weights have taller uppercase letters than the heavier weights—with lowercase letters changing only slightly—but in the opposite direction(!?).

Where non-variable fonts have separate files for each combination of weight/style/etc—and therefore its own set of metrics—variable fonts only have a single set of font metrics for all variations. I.e. the metrics are static—not variable. This can be a problem as it means the metrics will only be correct for one—or some—of the variations.

And - this is also going to be a problem for font-size-adjust as it relies on those same metrics.

Easy fix: stay clear of inconsistent variable fonts—or use old-school non-variable fonts (if viable).

Hard fix: make metrics variable in variable fonts by specification—and wait for browsers to support it, font tools to implement it, and font designers to update their fonts accordingly…

Conclusion

font-size-adjust is really nice.

And in the absence of support, we can put in the tedious work to add a workable fallback.

But I can’t help but think that it is just treating a symptom.

What we need is a stricter standard for fonts and their metrics, weights, widths, etc.

We need fonts that give us all the information we need to get real typographical control—while also elevating accessibility tooling.

In a perfect world, font-weight would be directly related to stem width so that we could match weights across font pairings without guessing. Calculating proper contrast in accessibility tools would finally be feasible.

As a side note, the Geist font was initially released with stem width-based font-weight—an inspired move. But due to user feedback, they sadly reverted to the standard 100-900 range.

For accessibility purposes font-size is only a vague measure; readability and contrast cannot be derived purely from it. It requires something more along the lines of a ratio between font-size (as in height) and font-width—and font-weight on top. And then there are also the visual qualities of the font design that come into play; some typefaces are squiggly but decorative handwritten ones and some are minimalist functional sans-serifs.

The vast difference between uppercase and lowercase letters across fonts is also due to font expression.

What if font-size is always related 1:1 to x-height? At least it would become a measure on screen—for once.

But until then I will continue to dig out font metrics to do cool stuff.