Field notes · Field engineering

The CSS rule that stops images loading on iPhones

Setting overflow-x to hidden on html or body turns the document into a scroll container. Native lazy loading then measures against the wrong root, and in Safari the images below the fold never load at all. Chrome usually hides the problem, so it looks like an Apple bug. It is not.

A client’s catalogue looked finished in Chrome and broken on an iPhone. Images below the first screen simply never appeared — no error, no broken-image icon, just empty space where the products should be. Because it only reproduced on Apple devices, the obvious conclusion was a Safari bug, and the obvious fix was to stop using native lazy loading. Both were wrong.

What is actually happening

loading="lazy" decides when to fetch an image by measuring it against a scroll root. Normally that root is the viewport. But overflow-x: hidden on html or body propagates to the document element and gives it a computed overflow-y: auto with a height equal to the full content. The document itself becomes the scroll container, and the intersection is now measured against something that is never “scrolled into view” in the way the browser expects.

WebKit is strict about this and never fires the fetch. Blink is forgiving and usually does. That difference is the entire reason it presents as an Apple-only fault, and it is why testing in Chrome gives you a clean bill of health on a genuinely broken page.

The tell is quick: switch one image to loading="eager". If it appears immediately, the images are fine and the lazy-loading root is the problem.

Why the rule was there in the first place

Nobody adds overflow-x: hidden to the body for fun. It is almost always there to suppress a horizontal scrollbar caused by some element overflowing on mobile — a one-line fix for a visible annoyance, applied months earlier by someone solving a different problem. That is what makes this class of defect expensive: the cause and the symptom are unrelated, separated in time, and each change was individually reasonable.

The fix

Use overflow-x: clip. It clips identically and creates no scroll container:

html, body { overflow-x: hidden; }        /* fallback for iOS < 16 */
@supports (overflow: clip) {
  html, body { overflow-x: clip; }        /* no scroll container; lazy-load works */
}

Then the part most people miss. The CSS fix alone is not enough, because JavaScript can reintroduce the exact same condition at runtime:

document.body.style.overflow = 'hidden';   // shorthand — silently sets overflow-x too
document.body.style.overflowY = 'hidden';  // axis-specific — leaves overflow-x alone

That shorthand is everywhere: modals, drawers, mobile navigation. Anything that locks scrolling while an overlay is open. The stylesheet says clip, the inline style says hidden, and the inline style wins for as long as the menu is open. Images load on first paint, then stop working after a visitor opens and closes the menu once — which is close to impossible to reproduce by accident.

What changed permanently

Two greps now run before any site of mine ships: one for overflow-x:\s*hidden on html/body, and one for .style.overflow\s*= anywhere in the JavaScript. Both are part of the release gate rather than a habit, because a habit is exactly what fails when the person applying it is busy.

The broader rule I took from it: a fix that only holds while everyone remembers it isn’t a fix. Any defect worth diagnosing twice is worth converting into a check that runs without being invoked.


Evidence
Found on a live product catalogue: every lazy-loaded image on the home page stayed blank on iPhone and Safari while Chrome looked perfect. Reproduced in WebKit, then found again five days later as a JavaScript regression of the same rule.
First written
Last reviewed

Free to quote with attribution and a link to this page. If you think something here is wrong, tell me — a note that stays wrong is worse than one that was never written.

Working together

Client work starts with a conversation — no commitment, no pitch. If it turns out I'm not the right fit, I'll say so.