The two fonts Liberation Sans and Domitian (serif) overlaid and highlighting their relative size difference at 200 pt size.🅭

Improve legibility and reduce layout shifts with x-height adjustments

There’s more to setting the text size on your webpages than just the CSS font-size property. It only controls the size of majuscule (“uppercase”, e.g. “A”) letters, numbers, and punctuation. The size of minuscule (“lowercase”, e.g. “a”) letters is left up to the font. A font’s minuscule size metric is referred to as x-height; the height of an “x” letter. However, you can control the size of the two independently — and here’s why you might want to do that.

The font-size-adjust property compliments the font-size property by letting you override the font’s x-height. Its value is a multiplier applied to latter to the font-size. For example, the CSS snippet font-size: 16px; font-size-adjust: 0.5; sets the x-height to 8 px.

Unfortunately, font-size-adjust is only supported in Firefox. It has been supported by this browser for over a decade already. It was implemented in Chrome for almost half a decade, but it has been left to rot behind the Experimental Web Platform features flag. It’s not implemented in Safari.

There’s another way that’s supported cross-browsers that lets you achieve even better results. However, I first need to explain the two main use cases for why you might want to adjust a font’s x-height.

The first use case is to increase legibility with your preferred font. A taller x-height (to a point) can increase reading speed, lower error-rates, and benefit readers with reduced vision. Tall x-heights leaves more room for each letter; which is especially beneficial for common double-story letters like the minuscule “a” and “e” that can otherwise appear cramped. Special-purpose fonts optimized for dyslexic readers feature a taller-than-normal x-height. However, a notable study concluded that these fonts are no better than traditional fonts when accounting for the different x-heights.

Common fonts, such as the pre-installed fonts on most operating systems (“system fonts”), typically feature a low x-height. Depending on your stylistic font preferences, you might just want to bump up the x-height to improve the reading experience.

There’s now a medium-sized confused-looking elephant in the room, though. Why can’t you simply set a bigger font-size if you want the text to be bigger and more readable? The most commonly used fonts today, the typical system fonts based on their broad availability, are based on digitalization efforts of decades-old font designs.

Today, there’s simply a different design and reading preference when it comes to the size-relation between majuscule and minuscule letters. More recent font faces — such as Google’s Roboto, designed for a varying quality of mobile-display technologies — feature a tall x-height.

The other use case is to better match a fallback font, such as a system font, to your preferred font, such as a web font. It can also be useful to normalize the reading experience across a system font stack based on a selection of metrically diverse fonts. It’s particularly useful when mixing letters from more than one font, such as a text font and a symbols font.

This use case might even apply if you only rely on system fonts. For example, the sans-serif fonts Helvetica and Arial look roughly 10 % larger than serif Times and Times New Roman at the same font-size.

To meet the requirements of these use cases, you need to individually adjust the sizes of each font face. Enter the @font-face { size-adjust } property. It lets you override a given font face’s x-height by a given percentage value. Using a variable font? Check its documentation if it supports adjusting the x-height through @font-face { font-variation-settings } parameter. The size-adjust parameter might be ignored if it conflicts with the font’s internal settings. For example, size-adjust: 105% to increase the minuscule size by 5 %.

Both Firefox and Chrome added support for this property at the end of 2021. There’s no support in Safari yet. However, this is still the best cross-browser method.

Okay, back on track. Here’s a quick example using the perfect cross-platform sans-serif font stack (a cross-platform set of metrically compatible fonts) where I’ve increased the minuscule size by 5 %. Roboto, the only font available on Android, was designed with a taller x-height than the others. So, it needs to be tuned separately to match the other fonts.

@font-face {
  font-family: "helveticas-adjusted";
  src: local("Helvetica Neue"),
       local("Arial Nova"),
       local("Liberation Sans"),
       local("Helvetica"),
       local("Arial");
  size-adjust: 105.2%;
}

@font-face {
  font-family: "roboto-adjusted";
  src: local("Roboto");
  size-adjust: 104.4%;
}

body {
  font-family: "helveticas-adjusted",
               "roboto-adjusted",
               sans-serif;
}

The example shows that this is quite a bit more complex than the simpler single font-size-adjust parameter. However, it lets you optimize individual fonts for optical sizes rather than being constrained to a single absolute size for all fonts.

You can extrapolate from this example into more complex font stacks. Here’s an alternative featuring an ordered list of my favorite serif system fonts from Windows, MacOS, and Linux. Their sizes vary considerably, but the size-adjust parameter normalizes their x-heights to be the same as the above sans-serif font stack.

@font-face {
  font-family: "constantia-adjusted";
  src: local("Constantia");
  size-adjust: 117.6%;
}

@font-face {
  font-family: "hoefler-text-adjusted";
  src: local("Hoefler Text");
  size-adjust: 121.6%;
}

@font-face {
  font-family: "charter-adjusted";
  src: local("Charis SIL"),
       local("Charter Roman");
  size-adjust: 112%;
}

@font-face {
  font-family: "times-adjusted";
  src: local("Liberation Serif"),
       local("Tinos"),
       local("Times"),
       local("Times New Roman");
  size-adjust: 115.4%;
}

@font-face {
  font-family: "palatino-adjusted";
  src: local("Domitian-Roman"),
       local("P052-Roman"),
       local("Palatino"),
       local("Palatino Linotype");
  size-adjust: 116%;
}

p {
  font-family: "constantia-adjusted",
               "hoefler-text-adjusted",
               "charter-adjusted",
               "palatino-adjusted",
               "times-adjusted",
               serif;
}

At first glance, this appears like quite a lot of style sheet bytes. It’s almost 1 kB uncompressed and 0,3 kB compressed by itself. You can expect the compressed size to shrink considerably when included as part of a larger style sheet. At any rate, it’s a tiny amount of data to fix up and normalize some system fonts compared to downloading a webfont file. Webfonts in had a median file size of 141 kB, according to the HTTP Archive.

Okay, so I’ve glossed over one subject so far: how do you figure out the number value for the size-adjust property for different fonts? Start by selecting your fonts and arranging your font stack. Once you’ve nailed it down, you can start measuring your fonts.

Adjust your primary font’s x-height until you get the size you want. In this article, I’ve argued for increasing x-heights to improve readability. But it’s your show: maybe you’re happy with your font’s default.

Create a test page with a minuscule “x” letter in three different sizes. Use your normal body text size, your headline size, and one extra-large size. Do the same for the font you want to size-match and overlay it on top of your reference size. It’ll be easier to compare the two if you assign the sets distinct colors.

Adjust the size-adjust property until the two match each other’s height (sans any serifs/ornamentation). It takes a bit of trial and error, but it shouldn’t take much time. It’s super-quick to repeat the process with different fonts after you’ve done it once.

You can find specialized tools to automate the job, but I wasn’t happy with the results and preferred doing it by hand. Automated tools may pick a mathematically optimal size adjustment, but it will fail to account for any perceived optical size differences between two fonts.

Once you’re done, you should double-check your design to make sure you’ve set aside sufficient line heights! The character boxes for your font’s minuscule characters will be stretched to accommodate their adjusted size. The new character box sizes will be the point size (font-size) multiplied by the adjust-size, e.g. 16 px * 106.25 % = 17 px. Your line-height must be at least that tall plus leave enough room for comfortable reading.

This technique can significantly reduce layout shift and other eyesores when a fallback font is displayed initially and then later swapped out for a webfont (font-display: swap or font-display: fallback).

Okay, so what about Safari? Your only two options are to either set a larger font-size or to pick a font with a tall internal x-height. The open-source Merriweather font by Sorkin Type delivered as a webfont is one such alternative. However, you’re back to square one when it comes to its fallback font.

You may also want to avoid using MacOS and iOS system fonts with short x-heights, such as Hoefler Text. Users on iOS are forced to use Safari and users on MacOS are likely to use Safari until WebKit bug #229726 is resolved. This isn’t a critique of that font design, but it’s unsuitable as its x-height requires significantly more upscaling than other fonts.

The issue with Safari highlights the importance of the size-adjust technique discussed in this article. It lets you decide what font you want to use, and adopt it to your needs; rather than the other way around.