If you’ve ever set position: sticky on a sidebar menu or table of contents and watched it just… not stick, you’re not alone.

In this article I’ll walk through a real case I ran into, including:
- The actual reason sticky positioning fails
- How to properly diagnose it in DevTools
- Why overflow on
htmlandbodywas the culprit — and how to fix it
with a reproducible step-by-step breakdown.
The common misunderstanding about position: sticky
You might have CSS that looks perfectly correct, like this:
.category-toc {
position: sticky;
top: 100px;
}And still run into the same complaints:
position: stickyis set, but nothing happenstopis defined- No
overflow: hiddenon any parent element — or so you think
If it’s still not sticking, there’s a good chance the declaration itself is fine, but something structural is silently killing it.
What to check first (diagnosing with DevTools)
Open DevTools in Chrome (or Edge), go to the Console tab, and run:
const el = document.querySelector('.category-toc');
getComputedStyle(el).position
getComputedStyle(el).top
getComputedStyle(el).overflow[!NOTE] If this is your first time pasting into the console, you may need to type “allow pasting” manually before Chrome lets you paste.
What a healthy result looks like
position: "sticky"
top: "100px"
overflow: "visible"If these values look right, the sticky declaration itself isn’t the problem.
If it’s still not sticking after confirming this, the next suspect is elsewhere.
The most common root cause: overflow on html / body
In my case, the culprit was the overflow settings on html and body.
Checking the console revealed something like this:
{
scrollingElement: 'html',
htmlOverflow: 'hidden auto',
bodyOverflow: 'auto'
}Which effectively meant:
html:overflow-x: hidden; overflow-y: autobody:overflow: auto
In other words, two separate scroll containers were active at once.
Why overflow on html/body breaks sticky
position: sticky calculates its position relative to:
- The nearest scrolling ancestor, or
- The viewport, if there isn’t one
But when:
htmlis scrollablebodyis also scrollable- And there’s a wrapper element on top of that
…the browser can’t reliably determine which container sticky should be anchored to, and the practical result is an element that appears to ignore sticky positioning entirely.
The fix: let html handle scrolling, and return body to normal flow
The most reliable fix is this:
html {
overflow-x: hidden;
overflow-y: auto;
}
body {
overflow: visible;
}Key points
- Consolidate vertical scrolling onto
htmlonly - Don’t set
overflow: autoonbody overflow-x: hiddenfor preventing horizontal scroll is fine to keep
The moment this change went in, the sticky element that had refused to work started functioning immediately.
Testing it live in the console first
Before touching your actual CSS, you can test this behavior temporarily in the console:
document.documentElement.style.overflowY = 'auto';
document.documentElement.style.overflowX = 'hidden';
document.body.style.overflow = 'visible';If sticky starts working after running this, you’ve confirmed overflow is the cause.
Watch out for these common regressions
Sticky positioning can break again if any of the following show up later in your codebase:
- Modal implementations that leave
html { overflow: hidden }on permanently - Layout code that sets
body { overflow: auto }for unrelated reasons - A
height: 100vhwrapper combined with its own overflow control
If you need to lock scrolling while a modal is open, it’s safer to keep overflow: auto as the default state and only switch to hidden while the modal is active:
html {
overflow-y: auto;
overflow-x: hidden;
}
.modal-open html {
overflow: hidden;
}Summary: what to check when sticky won’t stick
- Don’t assume you’re safe just because
positionandtoplook correct - Check
overflowonhtmlandbody, not just the immediate parent - Make sure there’s only one active scroll container on the page
More often than not, sticky positioning issues come down to where the page is actually scrolling from rather than a mistake in how sticky itself is written.
Hopefully this saves someone else the same debugging session.
