WebTech1223411 logo WebTech1223411Web tech, read closely
Front-End

Why position: sticky Stops Working and How to Find the Cause

You add position: sticky and top: 0 to a header, scroll the page, and it slides away with everything else as if you had written nothing. You check the spelling. You try a higher z-index.

A web page layout with a header pinned to the top while content scrolls beneath it

You add position: sticky and top: 0 to a header, scroll the page, and it slides away with everything else as if you had written nothing. You check the spelling. You try a higher z-index. You copy a snippet from a tutorial that definitely works there, and it still does nothing in your project. Twenty minutes later you are wondering whether to fall back to a scroll listener in JavaScript.

Don't. In years of cross-browser testing I have almost never seen sticky positioning fail because of a browser bug. It fails because of something higher up the page, usually written by someone else, often months earlier. The property is doing exactly what the specification says. The trick is learning where to look, and DevTools will show you in about two minutes once you know the pattern.

How sticky actually decides where to stick

A sticky element behaves like a normal element until its edge reaches the threshold you set, such as top: 0. From then on it holds that position, but only while two conditions are met.

First, it sticks relative to its nearest scrolling ancestor, not to the window. If any element between it and the root has overflow set to hidden, auto or scroll, that element becomes the reference. When that ancestor does not actually scroll, because it grows to fit its content, the sticky element has nothing to stick to and simply moves with the page.

Second, it can never leave its parent box. A sticky element travels only within the height of its containing block. If the parent is exactly as tall as the sticky element itself, there is zero room to move, and it looks completely broken.

Nearly every sticky problem I have debugged was one of these two, in some disguise.

The usual suspect: overflow-x on the body

The most common cause by a long way is a line like overflow-x: hidden on html, body or a page wrapper. Somebody added it to stop a stray element from causing horizontal scroll on phones. It works for that, and it quietly breaks every sticky element below it.

The reason surprises people. In CSS, if you set overflow on one axis to hidden and leave the other as visible, the browser computes the visible one as auto. So overflow-x: hidden really means overflow-x: hidden plus overflow-y: auto, and the wrapper becomes a scroll container. Look at the Computed tab in DevTools and you will see it there in black and white.

The cleanest fix is overflow-x: clip. Clip cuts off the overflowing content just like hidden, but it does not create a scroll container, so sticky keeps working. It is supported in Chrome and Edge from version 90, Firefox from 81 and Safari from 16. If you need to support older Safari, find the element that overflows and fix its width instead, which is the better long-term answer anyway.

Sticky is rarely broken by the element you are looking at. It is broken by an ancestor you forgot existed.

The other disguises

A flex or grid parent stretching the sticky child

A classic case is a sidebar inside a flex row. By default flex items stretch to the full height of the row, so the sticky sidebar becomes as tall as the main content. It now fills its whole parent and has no room to travel. Add align-self: start to the sidebar, or align-items: start to the row, and it immediately begins to stick. The same applies in grid layouts, where align-self: start does the job.

A wrapper that is only as tall as the header

Frameworks and page builders love wrapping elements in extra divs. If your header sits inside a header-wrapper div that is exactly the header's height, the header can stick only within that wrapper, which is to say not at all. Move the sticky rule to the wrapper itself, or remove the wrapper.

No threshold at all

Position: sticky without top, bottom, left or right does nothing, because the browser has no edge to stick at. This sounds too obvious to mention, but it happens when a threshold lives in a media query that does not apply at the current width, or is overridden by a more specific rule somewhere else. If your stylesheets fight over specificity, our piece on why CSS specificity battles make stylesheets harder to maintain explains how that happens.

Myths that waste time

Two pieces of advice come up again and again in forum threads, and both send people in the wrong direction.

The first is that a transform on an ancestor breaks sticky. That is true for position: fixed, where a transform on an ancestor changes what the element is fixed to. It is not true for sticky. You can stop hunting for transforms.

The second is that raising the z-index will help. Z-index only controls which element paints on top when they overlap. If your sticky header is scrolling away, z-index is not involved. It becomes relevant only after sticky works, when content scrolls over the header instead of under it.

A two-minute DevTools routine

This is the order I check in, on any browser. It takes less time than reading a Stack Overflow thread.

  1. Select the sticky element in the Elements panel and open the Computed tab. Confirm that position is sticky and that top or bottom has a real value at the current viewport width.
  2. Look at the height of the element and the height of its parent in the box model view. If they are the same, the parent is your problem.
  3. Click each ancestor in turn, from the parent up to html, and type "overflow" into the Computed filter. The first ancestor with anything other than visible or clip is the scroll container your element is stuck to.
  4. Check whether that container actually scrolls. If it does not, either remove its overflow rule, switch it to clip, or make it the deliberate scroller with a fixed height.
  5. If the parent is a flex or grid container, check align-items and align-self. Stretch is the default and the likely culprit.

Firefox also marks scroll containers with a small "scroll" badge in its Inspector, and recent Chrome releases have added a similar badge, which makes step three faster when your browser has it. There are a few other panel features like that, which most of us never notice, collected in our article on DevTools features most developers never open.

Sticky table headers, briefly

Tables used to be a special case, because some browsers would not make thead or tr sticky. Today the reliable approach is to put position: sticky and top: 0 on each th cell rather than on the row. If the table sits inside a div with overflow: auto to allow horizontal scrolling on phones, remember that the div is now the scroll container. The headers will stick to the top of that div, not the window, so give the div a maximum height if you want them to stay visible.

Try this on your own codebase

Search your stylesheets this week for "overflow-x: hidden" and "overflow: hidden" on html, body and any full-page wrapper. For each one, ask what horizontal overflow it was hiding, and whether overflow-x: clip, or fixing the element that overflowed, would do the same job without turning the wrapper into a scroll container. You may fix a sticky bug nobody has reported yet. For more on layout and CSS, browse our front-end articles.

MC
Maren Castellanos

Maren worked in cross-browser QA before editing, and knows the DevTools Network tab better than most people know their inbox. She writes about how browsers load and render pages and about the tools that show what is really happening.

More posts by Maren

More in Front-End

01doorganico 084goad 3hot8 4465456 after5internprogram ahandfulofblessings ahmadpc ahomcybersecurity allodialresources annodex antiquesandperiodcollectibles aotc asianworldkashmir beedlectrics bloggertemplatefree bluefeatheragency bodyweightsolution campaign08blog carfixadvisor catstreetcollective cbstvsports cnfanspurchasing committeeof500yearsofdignityandresistance covertopsdjs cs-kaiyuangame cuestacomunicaciontotal cxoutlet dancebetgame deskgearhub destinyvillastay detectmagazine di33 diamondsteelbuildings donmo dyanadropship emarsysfinance emimattl escapechan eskitarih evautoplay everythingweddings-orlando farmzone feedviewerapp foxstudiosscratch gamevui123 gleasongrade greetingsday hacklabasu haskell iw-kaiyun jagoanproperti jilidskripsi joltbusinessfinance juegodepokergratis juegospucca kaeru-festival latestmehndidesigns lestanzediottaviano litemagazines localcommunityhero lustrsazan mariashebets menswheels newwebstechs1 nikkit ootheday oxigenvpn plurfitness prefetial probudgetcarpetcleaning psychicmrsmartin pyaten qvantor rejoignezlogis rexzbot riselayouts ruchin rwxqsgd5 sakaltime shibiaowang shop-bot skrtips strcoding tetapatrium topselller uneplanteartificielle vipertechcr wo-kygaming yamaha-mekar yeknava yoybuysheets zenunwa yamaha-mekar jilidskripsi ankertoto ankertoto ankertoto ankertoto