You add position: sticky and top: 0 to your header. You scroll. The header scrolls away like nothing happened.
No error in the console. No warning in DevTools. Just a header that won't stick.
This post explains the usual reason, shows a fix you can paste in, and gives you a small script that finds the problem for you.
The problem
Someone posted this in a CSS forum. Here is the gist, in our own words.
They wanted the header on their page to stay at the top while the user scrolled. They added position: sticky. Nothing changed. They had already worked through a popular checklist of "why sticky isn't working": siblings, parent height, z-index, browser support. Still no luck.
The answer that solved it was one line far away from the header: a wrapper higher up the page had overflow: hidden.
Quick version of how sticky works
A sticky element acts like a normal element until you scroll it to a set point. Then it "sticks" there, until its parent box scrolls out of view.
Two key words here:
-
Scroll container: a box that can scroll its content. Elements with
overflowset tohidden,auto, orscrollcan be one. - Containing block: for our purposes, the parent box that the sticky element lives inside.
Root cause 1: an ancestor with overflow: hidden
MDN says a sticky element sticks to its nearest ancestor that has a "scrolling mechanism." That is created when overflow is hidden, scroll, auto, or overlay. It sticks to that ancestor even if that ancestor never actually scrolls.
So here is what happens:
- A wrapper has
overflow: hidden, maybe to clip a rounded corner or stop a sideways scrollbar. - The sticky header now sticks to that wrapper, not to the page.
- The wrapper never scrolls. The page does.
- The header rides along with the wrapper and scrolls off the screen.
Here is a small example that shows it:
<div class="page">
<header class="header">My site</header>
<main class="content">…lots of content…</main>
</div>
.page {
overflow: hidden; /* the hidden culprit */
}
.header {
position: sticky;
top: 0;
z-index: 1;
background: white;
}
We tested this in Chrome 156 and Firefox 157. After scrolling 500px, the header's top edge was at -500px. It scrolled away completely.
Fix A: remove the overflow
If the overflow: hidden isn't doing anything important, delete it. In our test the header then stayed at the top (top edge at 0px).
Fix B: use overflow: clip instead
Often the overflow is there for a reason, like hiding content that sticks out. In that case, use clip:
.page {
overflow: clip;
}
MDN explains the difference. hidden gives the element a scrolling mechanism, so sticky treats it as a scroll container. clip also hides the overflow, but the element is not a scroll container. So the sticky header skips it and sticks to the page. In our test this fixed the header in both browsers.
The trade-off: with clip, nothing inside can be scrolled into view, not even with code. MDN warns that if there are focusable elements in the clipped area, keyboard users may not be able to reach them. Only use clip when the clipped part has nothing interactive.
overflow: clip support, per MDN's browser data: Chrome 90, Firefox 81, Safari 16.
Fix C: move the overflow somewhere else
If you need hidden (for example, you scroll that box with code), put it on an element that is not an ancestor of the sticky element. For example, wrap only the content that needs clipping, and keep the header outside that wrapper.
The sneaky one: overflow-x: hidden
Many sites add this to stop a sideways scrollbar:
.page { overflow-x: hidden; }
"But I only hid the x direction!" you say. Here is the catch. MDN explains that when one direction is visible and the other is not visible or clip, the visible one behaves as auto. So overflow-x: hidden quietly turns overflow-y into auto. Now the box is a scroll container, and sticky breaks.
The fix is to use clip for that direction:
.page { overflow-x: clip; }
clip paired with visible stays as is, so no scroll container is made. We tested both: overflow-x: hidden broke the header, and overflow-x: clip fixed it.
What about html and body?
This one surprised us, so we tested it too:
| Setup | Header sticks? (Chrome 156, Firefox 157) |
|---|---|
overflow-x: hidden on body only |
Yes |
overflow-x: hidden on both html and body
|
No |
overflow-x: clip on both html and body
|
Yes |
When only body has it, browsers apply that overflow to the viewport (the browser window) instead, so sticky still works. When html has it too, body keeps its own overflow and becomes the trap. If you want a rule you never have to think about, use overflow-x: clip.
Root cause 2: no offset set
MDN notes that at least one inset (like top, bottom, left, or right) must be set to something other than auto. Without it, sticky acts just like relative. We removed top: 0 in our test, and the header scrolled away.
.header {
position: sticky;
top: 0; /* required */
}
Root cause 3: the parent is too short
A sticky element can only stick while its parent box is on screen. A common setup looks like this:
<header class="site-header">
<h1>Site title</h1>
<nav class="nav">…</nav> <!-- sticky -->
</header>
<main>…</main>
The nav is sticky, but its parent is the header. Once the header scrolls away, the nav goes with it. In our test, the nav was at -440px after a 500px scroll.
The fix: move the sticky element so its parent is the tall, scrolling part of the page. Here, put the nav after the header, as a sibling of main.
How to verify
1. Find the trap with a script.
Paste this into the browser console. Change the selector to match your sticky element:
(() => {
const el = document.querySelector('.header'); // your sticky element
for (let node = el.parentElement; node; node = node.parentElement) {
const s = getComputedStyle(node);
if (/(hidden|auto|scroll|overlay)/.test(s.overflowX + ' ' + s.overflowY)) {
console.log('Sticky is trapped by:', node, s.overflowX, s.overflowY);
}
}
})();
It lists every ancestor that can trap your sticky element. Click a result to jump to it in the Elements panel. One note: if body shows up and html does not, the body's overflow may be going to the viewport. Check that case with the table above.
2. Check the computed styles.
Select a suspect element and open the Computed tab. Look at overflow-x and overflow-y. Remember that a value you didn't write (like auto) can show up because of the rule about pairs above.
3. Measure while scrolling.
Scroll down, then run:
document.querySelector('.header').getBoundingClientRect().top
If it sticks, you'll see your top value, such as 0. If you see a negative number, it scrolled away.
4. Test on a real page length.
Sticky only shows up when there's enough content to scroll. Test with real content, not a near-empty page.
Checklist
- [ ] The sticky element has
top(or another inset) set. - [ ] No ancestor has
overflow: hidden,auto, orscroll, unless you want it to stick inside that box. - [ ] Use
overflow: cliporoverflow-x: clipwhen you only need clipping. - [ ] The sticky element's parent is tall enough to scroll through.
- [ ] Add a
backgroundandz-indexso content doesn't show through or sit above it.
Sources
- MDN:
position— https://developer.mozilla.org/en-US/docs/Web/CSS/position - MDN:
overflow— https://developer.mozilla.org/en-US/docs/Web/CSS/overflow - MDN browser compat data (
overflow: clip,position: sticky) — https://github.com/mdn/browser-compat-data
Shared by PIXEL RECRUITING LLC, where we match companies with frontend, UI and design systems engineers. Direct staffing partner for Mastercard · pixelrecruiting.com · info@pixelrecruiting.com
Top comments (1)
The overflow: clip fix is the part I'd have missed. Overflow: hidden creating a scroll container even when nothing scrolls explains so many sticky headers that silently fail. One tip: if you can't remember which wrapper is the culprit, walk up parentElement in DevTools and log getComputedStyle(el).overflow for each ancestor. Have you seen overflow-x: hidden alone trigger the same thing, since the other axis computes to auto?