Why my code blocks had different font sizes on iPhone

Every code block on this blog has font-size: 1rem. On my iPhone, in the file input article, they came out in at least two sizes. The HTML example in Step 2 was so large that every line ran off the right edge. The CSS example right after it was the size I had set. Further down, a long JavaScript block was large again, and the one-line block under it was normal.

Two code blocks from the same article on an iPhone: the HTML block on the left has much larger text than the CSS block on the right
Before: Step 2 and Step 3 of the file input article in Safari on iPhone. Both blocks have font-size: 1rem.

Desktop Safari showed every block at the same size, and so did responsive mode in the browser's developer tools. Only the phone was different.

Safari makes some text bigger on its own #

Safari on iPhone has text autosizing. If it thinks some text is too small to read, it makes the text bigger, and the font-size in the CSS doesn't stop it. The feature is for desktop-only sites that a phone shrinks to fit the screen. My page isn't shrunk. The HTML block in Step 2 came out huge anyway, and the CSS block right under it didn't.

The reason is in the WebKit source. Safari doesn't check whether the page as a whole was shrunk. After each layout it walks the blocks on the page and compares each one with the width of the screen. A block that fits is left alone. For a block wider than the screen it raises the minimum font size: 15px times how many times wider the block is, with a cap on how far the text may grow. Only text with text-size-adjust: auto is changed, and that is the default. Then it averages the candidate sizes across text nodes whose styles match, font and line-height included, so enlarged blocks built the same way usually come out the same size. If you want to dig into the engine, the places to read are RenderBlockFlow::adjustFontSizes and TextAutoSizing.cpp, and the screen width reaches the page from WebPageProxyIOS.mm.

In my code blocks, the code element is display: block with inline-size: max-content, so the pre around it can scroll sideways. A block with a long line is therefore a block wider than the screen. I measured three blocks from the start of the file input article at 16px and ran them through the same formula: 15px times the block width divided by the screen width. My iPhone's screen is 390px.

Block Longest line Width Minimum
HTML, Step 2 58 characters 559px 21–22px
CSS, Step 3 42 characters 405px ≈ 16px
One-line JS, Step 4 39 characters 376px fits the screen

The HTML block's minimum comes out at 21 or 22px, depending on how the width rounds, so it goes on the list of blocks to enlarge. The size it ends up with is averaged with the other wide blocks on the page, so it can be larger than that. The CSS block is also wider than the screen, but its minimum rounds to the 16px it already had, so it never makes the list. The one-line block fits on the screen, and Safari doesn't consider it at all. I measured in a desktop browser and read the current source, not the build on my phone, but it matches all three screenshots, and the demo below behaves the same way on the phone itself.

I found a similar report in the WebKit bug tracker. In bug 304640, on iOS 18.7.2, the first line of a code example is 13px and the next two are 20px, while the Mac shows them the same. The reporter set font-size: 13px on the third one directly, and it still came out 20px. Simon Fraser from the WebKit team answered that this is text autosizing and that -webkit-text-size-adjust: none turns it off. He gave the same answer in bug 309551.

See it on your phone #

This page has two code blocks at 16px, built the same way as the ones on this blog. The first has a line longer than the screen. On my iPhone its text came out 1.6 times larger, 26px instead of 16. That is the cap: for a block that wide the width formula alone would ask for far more, but WebKit also multiplies the font size by a built-in 1.95 divided by the base-10 logarithm of the size, which for 16px is 1.62, and takes the smaller of the two. The checkbox adds the rule from the next section and reloads the page.

Open the demo on its own if the frame is too narrow. The label above each block shows its width at 16px, the screen width, and the size the text actually came out at.

The fix #

CSS
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

With 100%, text should stay at the size the CSS gives it. The property is inherited, so I put it on html and it reaches every code block. Safari only reads the prefixed name. The unprefixed one is for Chrome on Android and other browsers that support it.

If your project uses Normalize.css or Tailwind's Preflight, this line is already on your html. That is probably why I had never run into it before: this blog has no reset.

The same two code blocks after the change, with text of the same size
After: the same two places with text-size-adjust: 100% on html.
The long JavaScript block from the file input article: large text on the left, normal text on the right
The long JavaScript block, without the rule on the left and with it on the right.

The rule doesn't take zoom away from the reader. With it in place, I could still pinch to zoom on my iPhone, and Page Zoom in the aA menu still made all the text bigger, code blocks included.

What the property is for #

-webkit-text-size-adjust comes from Apple. Its archived Safari Web Content Guide explains it with double-tap. On a page made for desktop, a double-tap on a block fits the block's width to the screen. Safari picks a text size for that block when it first renders the page, so the text is readable after the double-tap. If that breaks a page, the guide says to turn it off with none or set your own percentage. The guide listed auto as the default on iPhone and none on iPad.

The unprefixed name comes from the CSS Mobile Text Size Adjustment draft, still an Editor's Draft. It lists white-space: pre among the conditions that stop the enlarging, and my code blocks use that value and were enlarged anyway. WebKit also has a newer autosizing algorithm, AutosizeStatus::compute. It is on by default only on devices that aren't small-screen, which means iPad, and it does nothing when the page's initial scale is 1. The iPhone runs the older algorithm, which never looks at white-space.

100% or none #

The CSS draft treats 100% and none as the same thing. So does the algorithm that enlarged my blocks: it only touches text whose value is auto. The difference exists in the newer iPad algorithm, where some elements can still be enlarged at 100%, for example floated ones. The WebKit team suggests none in both bug reports. I used 100%, the value the resets use, and on my iPhone every code block in the file input article came out the same size.

Why desktop testing misses it #

Text autosizing is only on in iOS. In WebKit's preference defaults it is enabled for the iOS family and off everywhere else. Desktop Safari runs with it off, and its responsive mode is the same desktop Safari, so neither could show me the problem. Now I open an article on the phone before publishing it.