D
P
0

CSS & Web Animation

`overflow-x: hidden` Kills `position: sticky` on iOS Safari — Switch to `overflow-x: clip`

June 26, 2026·4 min read
`overflow-x: hidden` Kills `position: sticky` on iOS Safari — Switch to `overflow-x: clip`

The sticky header on a custom theme I built worked perfectly in Chrome, Firefox, and even desktop Safari. Then the client opened the site on an iPhone, and the header that was supposed to pin to the top while scrolling just... sat there. It scrolled away with everything else, as if position: sticky had never been applied. The side rail that was meant to stay pinned did the same. No console error, no warning, nothing. iOS Safari was simply pretending sticky was an unknown value.

What made it maddening was that the code was obviously correct. The element had position: sticky and top: 0, both perfectly valid, and in every other browser it pinned flawlessly.

.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
}

I borrowed a friend's phone to make sure this wasn't one weird device. Same on every iPhone. After a few hours diffing a working branch against the broken one, the culprit turned up — and it was hiding somewhere I never thought to look. Not in the header. In body.

A few days earlier I had patched an unrelated problem: a tiny horizontal scroll, maybe a pixel or two, caused by an element that bled slightly past the viewport. The reflex fix everyone reaches for:

body {
  overflow-x: hidden;
}

The horizontal scroll vanished. I thought the case was closed. That one line was what had quietly killed sticky on iOS.

Why this happens

The heart of the problem is what overflow: hidden actually does behind the scenes. We tend to read overflow-x: hidden as simply "crop anything that bleeds out sideways." It does more than that: any overflow value other than visible — including hidden, auto, and scroll — turns that element into a scroll container.

The moment an ancestor becomes a scroll container, it changes the scrolling context for every descendant. position: sticky works relative to its nearest scrolling ancestor — that is where it computes when to start sticking. By putting overflow-x: hidden on body, I had unknowingly shifted which scroll container was relevant to the header. Its containing block and its scrolling context changed.

On most desktop browsers this shift is absorbed gracefully and sticky keeps working. iOS Safari treats it differently: when an ancestor becomes a scroll container via overflow: hidden, sticky behavior on its descendants commonly just dies. There is no error, no visible fallback — sticky simply stops sticking and behaves like a normal positioned element.

That is why the debugging was so misleading. The header's own CSS was not wrong in any way. The wrong thing was a single property on a distant ancestor far above it, written for a completely unrelated reason, days before.

The fix

The solution is one word: clip, not hidden.

body {
  overflow-x: clip;
}

overflow-x: clip crops horizontal overflow exactly like hidden does — that annoying one-or-two-pixel scroll still disappears. The crucial difference is that clip does not turn the element into a scroll container. It just crops what pokes out, without creating a scrollable region and without changing the scrolling ancestor for its descendants.

Because no new scroll container is established, the header's scrolling context stays intact. position: sticky gets its correct reference back, and on iOS Safari it immediately starts pinning again the way it should. One word swapped, two problems solved at once: the overflow stays cropped and sticky stays alive.

One caveat worth knowing: clip has slightly less legacy-browser support than hidden. In modern browsers it is well supported, so for most projects this is a non-issue. If you need a specific clipping distance — say you want a little breathing room before content is actually cropped — pair it with overflow-clip-margin:

body {
  overflow-x: clip;
  overflow-clip-margin: 1rem;
}

If you genuinely still have to support ancient browsers that cannot use clip, the better alternative is to chase down the source of the overflow directly — find the element that bleeds out and constrain its width or fix its margin — rather than papering over it with overflow: hidden on an ancestor that contains sticky elements.

The takeaway

clip is not a synonym for hidden. Both visually hide overflow, but hidden carries an invisible side effect: it creates a scroll container, and that scroll container can break position: sticky, scroll snapping, and other positioning contexts — especially on iOS Safari, which is the fussiest about it.

The rule I follow now: if all I want is to crop overflow and not create a scrollable area, I reach for overflow: clip. I save overflow: hidden for cases where I deliberately want a scroll container, or where I am explicitly disabling scroll. Since separating those two intentions, the whole "why is sticky dead only on iPhone" category of bug has disappeared from my life. And when something breaks only on iOS Safari, the first place I look is no longer the broken element — it is the overflow on every ancestor above it.