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
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
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
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?
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.
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.
Update 2024-08-11: font-size-adjust
is now part of Baseline which means it’s available in Chrome 127+. Yay!
Legacy support
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 baking a cake; you can’t unbake them (get them out).
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 thehead
table of the font.sxHeight
: Thex-height
. Located in theOS/2
table.sCapHeight
: Thecap-height
. Also located in theOS/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
Units per em | 1000 |
---|---|
Cap-height | 718 |
X-height | 538 |
--cap-height | 0.718 |
---|---|
--x-height | 0.538 |
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 thefont-size
.X-height
: Displays how the type size is changed to match lowercase letters tofont-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
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.
Related links
em
unit on MDN- Johannes Gutenberg
- Movable type
font-size-adjust
on MDNfont-size-adjust
support on CanIUse- Deep dive CSS: font metrics, line-height and vertical-align by Vincent De Oliveira
- Opentype.js Font Inspector
- Fontkit