D
P
0

HTML & CSS

`overflow-y: auto` on `body` Silently Kills Every `position: sticky`? The Body Became a Scroll Container

July 7, 2026·5 min read
`overflow-y: auto` on `body` Silently Kills Every `position: sticky`? The Body Became a Scroll Container

I was porting a client's scrollytelling page to a new stack. The page had several tall sections that were meant to "pin": you scroll in, the stage inside holds still on screen while you keep scrolling through a tall runway, then the next section takes over. The classic pin-and-scroll effect. In the old build it all worked. In the new build, after I reassembled the CSS, those sections rendered blank.

Not blank as in gone. The content was in the DOM, but nothing pinned. Every stage that was supposed to hold still just scrolled off screen early, leaving a long empty runway behind it. So you scroll, the stage flies past, and then you stare at empty space until the next section arrives. Empty gaps everywhere. From the outside it looked like the sections were broken, even though the markup and the runway heights were identical to the old version.

The symptom

What confused me at first: position: sticky was clearly there on the stage element. I inspected .section__inner, and there it was, position: sticky; top: 0. Not overridden, no typo, computed value was sticky too. Yet the element behaved as if it were position: static. No pinning at all.

My first instinct was a bad one, as usual. I assumed some ancestor had overflow: hidden clipping the sticky context, because that is the most common sticky-killer I run into. I walked up from .section__inner, checking every wrapper one by one. No overflow: hidden on that path. I also checked whether any ancestor had a stray transform or an odd height that sometimes shifts a sticky element's containing block. That came back clean too.

The strange part: this was not one broken section. Every pinned section on the page failed at once. When a bug hits all instances simultaneously, the cause is usually not in each component but in something global. That is what finally pointed me at the reset CSS.

The root cause

In the page's reset CSS, there was one line I had carried over from the old build without a second thought:

html, body {
  overflow-y: auto !important;
}

It looks harmless. The intent was just to make sure the page scrolls vertically. But this line was the culprit.

The moment you set overflow-y to anything other than visible, that element becomes a scroll container. Set overflow-y: auto on body, and body gets promoted to its own scroll container. The problem is that position: sticky only sticks within its nearest scrollport ancestor. As long as body is not a scroll container, that scrollport is the viewport, and sticky pins there, exactly what we want. But once body becomes a scroll container, it takes over the scrollport role for every one of its descendants.

And here is the trap. My stages were taller, or at least their runways made the sticky context taller, than body as a scrollport. A sticky element cannot pin when its containing block does not give it room to shift within its scrollport. Because body was now the scrollport and the pinned content overran how sticky computes its travel, sticky silently gave up. No error, no warning. It just stopped pinning and behaved like an ordinary element. That is why every stage scrolled off instead of holding still.

So it was not an ancestor's overflow: hidden that killed it, as I first guessed. What killed it was overflow-y: auto on body turning body into the scrollport, leaving sticky with no viewport scrollport to pin against.

The fix

The fix is to make body stop being a scroll container, so the viewport goes back to being the scrollport for every sticky element. I changed the reset line to:

html, body {
  overflow-y: visible !important;
  overflow-x: clip !important;
}

With overflow-y: visible, body stops being a scroll container. The scrollport returns to the viewport, and every .section__inner { position: sticky; top: 0 } pins again immediately. All the pinned sections recovered at once, from a single change. No component needed touching.

One important detail about overflow-x. I still needed to contain horizontal overflow so no stray horizontal scrollbar appears. But I deliberately used overflow-x: clip, not overflow-x: hidden. The reason: overflow-x: hidden on one axis silently forces the other axis to compute as auto, which means body becomes a scroll container again and sticky dies all over. overflow-x: clip clips the horizontal overflow without creating a new scroll container, so the sticky context stays intact. It is a subtle distinction, but a decisive one.

Checklist when position: sticky refuses to pin

  • Check overflow on html and body. If either overflow-y or overflow-x is anything but visible, that is likely what promoted the element to a scroll container and killed sticky.
  • If the bug hits every sticky element at once, suspect global or reset CSS, not the components.
  • Walk every ancestor for overflow: hidden, transform, or filter; each can clip or shift the sticky context.
  • To contain horizontal overflow without killing sticky, reach for overflow-x: clip, not overflow-x: hidden.
  • Remember the core rule: sticky only pins within its nearest scrollport ancestor, and setting a non-visible overflow on an element makes that element the scrollport.

Since then, when a sticky refuses to pin, the first thing I check is not the component but the overflow on html and body. Nine times out of ten, the answer is already sitting in a reset CSS I copied without thinking.