WebKit zoomed this blog out below 100% on an iPhone — overflow-wrap and a table scroll box fixed it
Pinch-zooming out on an iPhone shrank this blog below 100%. WebKit takes its zoom floor from content width, so the fix was overflow-wrap and a table scroll box.

On this page
Introduction
Pinching to zoom out on one of my own articles on my phone shrank it below its normal size, which felt wrong enough to look into.
The home page would not do it: pinch to zoom out there and the page stays where it is.
Both are served with the same <meta name="viewport">, so I doubted the tag was
the difference at all.
WebKit takes its minimum zoom-out scale from the document’s
real content width, and it has ignored minimum-scale since iOS 10, so there was
nothing to cap. The real problem is that the page overflows the phone.
Two CSS rules fixed it: overflow-wrap: anywhere on inline code, and a scroll
box around any table too wide for the body text. I also added a check that measures every article at a phone’s width, so a page that outgrows the screen fails before it ships.

My instinct was wrong: the viewport meta tag cannot cap zoom-out
There is no viewport value that stops a page zooming out. I read the screenshot as a meta tag missing a floor, and this site serves the minimal one on every page:
<meta name="viewport" content="width=device-width, initial-scale=1" />Adding minimum-scale=1 to that line is the fix I went looking for, but it does
nothing. Since iOS 10, WebKit has ignored user-scalable, minimum-scale and
maximum-scale outright, and the release post is blunt about it:
The instinct was also aimed at the wrong thing: zooming out is how a reader copes with content too wide to read. A page that forbids it could introduce more problems.
The home page was the clue — same meta tag, different content
The home page would not zoom out at all, and it is served from the same layout as every article. One tag, two behaviours, so the tag was not the variable. What differed was what the two pages put inside it.
That is checkable in one expression per page, and the numbers come back apart immediately:
INdocument.documentElement.scrollWidthOUT390INdocument.documentElement.scrollWidthOUT591clientWidth was 390 on both. A document 591 wide inside a 390 viewport is
201 pixels of content hanging off the side of the screen.
The same article reports 591 whether the window is 390 or 500 wide, which is the part that pointed away from the screen entirely. Its width was being set by something inside the article, and a number that ignores the viewport is not a number the viewport can fix.
The zoom floor = document’s own width
WebKit lays a page out at device-width, notices that the content overflows
that, and picks the scale that fits all of it on screen. Its own description of
the behaviour is that this is conceptually the same as the reader pinch-zooming
out far enough to see everything.
So the minimum scale is a quotient the page computes about itself: screen width over content width.
All of this was measured in Chrome on an iPhone, which is where the screenshot came from. The behaviour belongs to the engine rather than the browser: every iOS browser renders in WKWebView, so Safari and the rest inherit it.
Every number in the screenshot falls out of that quotient. A 591-pixel document on a 430-point screen floors at 0.73, and the worst page on this site floored at 0.38, because its document was 1028 pixels wide. Nothing about zoom needed adjusting; the quotient needed a smaller denominator.
What a sweep of the whole site found
One page’s overflow is a per-page fix, so the next question was how many pages
had it. All 356 pages in the production sitemap, loaded at 390 by 844, with
scrollWidth compared against clientWidth:
INnode --input-type=module -e "…" # playwright over every route in the sitemapOUTroutes measured: 356 overflowing: 63The offenders were not what the first page suggested. Grouped by the tag of every element whose right edge crossed the viewport:
| Element | Overflowing pages it appeared on |
|---|---|
code | 63 |
table and its cells | 52 |
a | 8 |
strong | 6 |
Inline code was on every one of them, and tables on 52. The article I had
been looking at happened to be a table case, and the worst page on the site has
no table at all: a Windows registry path in a paragraph, set as inline code,
reached 1009 pixels on its own.
INnode scripts/check-overflow.tsOUT right=1009 div.page > main > article > div.prose > p > code text: "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnloc"Expressive Code’s line spans cross the viewport on nearly every article, and they appear nowhere in that table. Why they are absent is the whole mechanism behind both fixes.
Nothing hides what a box cannot fit. The default, overflow: visible, means
content wider than its box is still drawn, and still counts towards how far the
page can scroll sideways. Every box outside it takes that width too, up to the
document. So a 1009-pixel string in a paragraph makes the whole document 1009
pixels wide.
overflow-x: auto is what refuses that width. A box with it set becomes a
scroll container: anything wider is clipped at its edge and scrolls inside it
instead. The boxes outside now see only that box’s own width, so the extra never
reaches them. The text is as wide as it ever was; the document stops counting
it.
That is why those line spans cannot widen the page. The <pre> they sit in
already scrolls, so their overflow dies at its edge, and a check that flags any
element crossing the viewport would report them on every article forever.
Wrapping the code: overflow-wrap, not break-word
A browser will not break a word that has nowhere to break, so a long token in
inline code makes the document as wide as the token itself.
The fix is one property, overflow-wrap: anywhere, on a selector this site
already had:
:not(pre) > code { background: var(--bg-2); border: 1px solid var(--border); border-radius: 4px; padding: 0.1em 0.35em; overflow-wrap: anywhere;}anywhere rather than break-word. Only anywhere shrinks the element’s
min-content width.
So it does more than wrap the paragraph case. It narrows every table cell that holds inline code, which here is most of them, and part of the table problem goes with it.
Boxing the table: two candidates, and what DevelopersIO ships
A table wide enough to overflow has two available fixes, and I wanted to know what a site that publishes tables every day does about it. DevelopersIO runs Japanese technical articles full of them, so one of its pages got measured the same way:
| dev.classmethod.jp | This blog, before the fix | |
|---|---|---|
| Viewport meta | width=device-width, initial-scale=1 | identical |
documentElement.scrollWidth at 390 | 390 | 591 to 1028 |
Computed display on a table | block | table |
Computed overflow-x on a table | auto | visible |
| Wrapper element around it | none | none |
Computed overflow-wrap on inline code | break-word | unset |
They ship the CSS-only fix, with no wrapper element anywhere: the table is made a block box and told to scroll. It works, it needs no build step, and it is two lines.
display: block, and what it costs a narrow table
The cost lands on the tables that were never too wide. A table whose content is narrower than the body text stops spanning it and shrinks to the width of its own text, leaving the rest of the line empty. Measured on this site’s own layout: 711 pixels of a 712-pixel text width before, 139 after.
display: block keeps only the width of its own text; a table already wider than the body text looks identical either way.DevelopersIO simply accepts that cost. Measured across eight of their articles, four of the seven tables are narrower than the body text and render shrunk against its left edge. Where a table’s content is wider, the two approaches are indistinguishable: those rows come back 655 pixels inside a 656-pixel text width.
The wrapper element, and why it won here
The other candidate wraps the table in an element that scrolls, leaving the
table itself alone. Every table keeps display: table, so nothing renders
differently and only the too-wide ones gain a scrollbar. That is what I chose,
and what settled it was finding the rule already written in this repo, unused:
/* Wide content must scroll in its own box, never the page body. */.table-scroll { overflow-x: auto;}Nothing in the repo applied that class. The intent had been recorded and the wiring never built, so the work was a rehype plugin that wraps each markdown table as the tree is built:
if (child.type === "element" && child.tagName === "table") { children[i] = { type: "element", tagName: "div", properties: { className: ["table-scroll"], tabIndex: 0 }, children: [child], };}markdown.rehypePlugins is deprecated, and says so on every run:
[astro] `markdown.remarkPlugins`, `markdown.rehypePlugins`, and`markdown.remarkRehype` are deprecated. Pass them to `unified({...})` from`@astrojs/markdown-remark` directly instead.import { unified } from "@astrojs/markdown-remark";
markdown: { processor: unified({ rehypePlugins: [rehypeTableScroll] }),},unified() is the Markdown processor itself, the pipeline that parses Markdown
and emits HTML. Astro’s export builds the same one it would have used anyway and
takes your plugin lists as arguments: remark plugins run on the Markdown tree,
rehype plugins on the HTML tree after it. rehypeTableScroll wraps a <table>
element, which only exists once the HTML tree does.
It uses the options you pass and fills in the rest from their defaults, so registering one rehype plugin switches nothing else off. GFM stays on, and GFM is the Markdown extension that adds table syntax, which plain Markdown has none of. Without it there would be no tables for the plugin to wrap.
The tabIndex: 0 in that snippet lets a reader tab to the scroll box and walk
a wide table’s far columns with the arrow keys.
The gate: every article measured at 390px
The sweep became a script, pnpm check:overflow, which loads every article in every locale plus the
utility pages at 390 by 844 and fails any page whose scrollWidth exceeds its
clientWidth:
INpnpm check:overflowOUTNothing overflows. 211 route(s) measured at 390px.I ran the same check against the site that had not been fixed yet:
INFIT_BASE_URL=https://oharu121.com pnpm check:overflowOUT63 route(s) wider than the viewport, of 191 measured.If any box above an element already scrolls or clips (a computed overflow-x
of auto, scroll or hidden), that element is skipped. That keeps Expressive
Code’s spans out of the report and leaves the table and the paragraph in.
The check also probes the feed’s later pages rather than computing how many
there are. That found 13 pages a count derived from POSTS_PER_PAGE would have
missed, taking the run from 198 to 211.
It is not part of pnpm check, because it needs a server running, which is the
same reason this repo’s figure check is excluded from it. The publish step runs
it instead, on the dev server it has already started.
Summary
A page that can be pinched below 100% on an iPhone is reporting its own
width, not a missing viewport value. WebKit floors the scale at screen over
content and has ignored minimum-scale since iOS 10, so the only lever is the
layout.
Two rules covered every case here. overflow-wrap: anywhere on inline code,
where anywhere rather than break-word also shrinks min-content and narrows
the table cells that hold code.
And a scroll box around a wide table, as a wrapper rather than display: block
on the table itself, so the tables that already fitted keep filling their
width. The wrapper takes tabindex="0", so a reader can tab to it and walk
the table’s far columns with the arrow keys.
The rest is measurement. scrollWidth against clientWidth at a phone’s width,
on every article rather than the one that prompted the question, with elements
inside a scroll container excluded because their overflow never reaches the
document.
References
- WebKit: New interaction behaviors in iOS 10, including that
user-scalable,minimum-scaleandmaximum-scaleare now ignored - MDN: the viewport meta element, and what each directive does
- MDN:
overflow-wrap, including howanywherediffers frombreak-wordon min-content sizing - Understanding WCAG 1.4.10 Reflow, the 320 by 256 CSS pixel requirement
- Apple: Configuring the viewport, for the
shrink-to-fitbehaviour this rests on

