May 18, 2026 · 3 min
When position:sticky silently dies
A scroll-driven 3D scene worked everywhere except where I needed it. The bug was three properties up the tree.
A pinned canvas. A tall scroll wrapper around it. The user scrolls, the camera scrubs through the scene. Standard pattern. The kind of thing that’s supposed to take ten minutes.
We shipped it. The first scene worked. Everything below the first scene was blank.
The canvas was set up right. position: sticky, top: 0. The wrapper had the right height. The DOM looked fine. DevTools’ Computed tab even agreed: position: sticky was applied, no warnings, no errors.
It just wasn’t sticking.
We went up the tree.
The bug was on the great-grandparent.
.demo {
position: relative;
min-height: 100svh;
overflow-x: hidden;
}
overflow-x: hidden. Set on a wrapper way upstream of the canvas, half-remembered from some other page where a fullscreen canvas was pushing horizontal scroll. The fix wasn’t needed here. The line was already there anyway.
Here’s the thing about overflow: hidden. Set it on any axis — -x, -y, both — and that element becomes a scroll container. Anything sticky inside it now sticks to that element’s bounds, not the viewport’s. So the canvas was technically sticking. Sticking inside a container that was itself scrolling past the viewport. By the time the user got to scene two, the sticky range was over and the canvas had unstuck politely and gone home.
Removed the three words. Worked.
What gets us about this kind of bug: position: sticky doesn’t fail loudly. There’s no console warning. The Computed tab still shows position: sticky proudly applied. It just doesn’t stick, and you’re left looking at an element that says it’s doing the thing while not doing it.
A non-exhaustive list of things that quietly kill sticky positioning:
overflowanywhere upstream, on any axis, set to anything that isn’tvisibletransform: anythingon an ancestor (creates a new containing block)filter: anything, same reasoncontain: paintorcontain: layout, same reason- A fixed-height ancestor that’s shorter than the sticky range needs
None of these throw errors. The element just sits there, agreeing with you that it’s sticky while being relative.
When sticky doesn’t work, the bug is somewhere upstream. Always. Walk the tree.
We always walk the tree.