Home / Blog / Position sticky not working

Why position: sticky Isn’t Working

Published August 28, 2026 · 11 min read · by the CSS DNA team

Quick answer: In roughly four cases out of five, an ancestor has overflow set to something other than visible — very often an overflow-x: hidden added to body months ago to kill a horizontal scrollbar. That ancestor becomes the scroll container the sticky element sticks to, and it never scrolls, so nothing happens. The fix is usually one word: overflow: clip instead of overflow: hidden.

Sticky positioning has no error state. There is no console warning, no visual fallback, no clue in the styles panel — the element simply behaves as if you had written position: relative. That silence is what makes it frustrating, because the cause is nearly always in an element you were not looking at.

The five causes, in the order they occur

#CauseFix
1An ancestor has overflow other than visibleRemove it, or switch to overflow: clip
2No threshold — none of top, bottom, left, right is setAdd top: 0
3The parent is no taller than the sticky elementGive the parent real height, or move the sticky element up a level
4The parent is a flex or grid container that shrank the itemalign-self: start on the sticky element
5Sticky applied to <tr> or <thead>Apply it to the <th> and <td> cells instead

Break it yourself

Sticky breaker

A real sticky header in a real scroll container. Turn on any cause and scroll the box — the header stops sticking exactly the way it does on your own page.

Sticky header · top: 0

↑ scroll inside the box

1. An ancestor creates a scroll container

A sticky element is positioned relative to its nearest scrolling ancestor. Setting overflow to hidden, auto or scroll on any element turns it into a scroll container — including hidden, which is scrollable programmatically even though it shows no scrollbars. So the sticky element latches onto that box instead of the page, and since that box never scrolls, the element never reaches its threshold.

Two things make this hard to find. First, it can be any ancestor, not just the parent — often five or six levels up. Second, the usual culprit is overflow-x: hidden on html or body, added to stop a horizontal scrollbar and forgotten. The axes are not independent here: setting overflow-x to a non-visible value forces overflow-y to compute to auto, so overflow-x: hidden alone is enough to create the scroll container.

The fix almost nobody mentions

You usually still want the clipping. You just do not want the scroll container. That is exactly what overflow: clip is for. From MDN:

“With clip, content that overflows is by default hidden, there are no scroll bars, and programmatic scrolling is not possible. The element is not a scroll container and no new formatting context is created.” — and, separately: “All values, except visible and clip, create a new block formatting context.”

So swapping one word restores sticky while keeping the overflow hidden:

/* breaks every sticky descendant */
body { overflow-x: hidden; }

/* clips identically, breaks nothing */
body { overflow-x: clip; }

Support is complete across evergreen browsers — Chrome and Firefox had it for years and Safari 16 finished the set in September 2022. If you still serve older Safari, the two-line progressive enhancement costs nothing, because a browser that does not understand clip ignores the second declaration and keeps the first:

body {
  overflow-x: hidden;
  overflow-x: clip;
}

One behavioural difference to know: clip forbids all scrolling, including programmatic scrolling and the browser scrolling a focused element into view. If content inside that box needs to be reachable by keyboard, clip is the wrong choice and you should remove the overflow instead.

Finding the ancestor is the hard part. CSS DNA shows the computed styles for an element and every ancestor above it in one panel, so an overflow six levels up is visible without clicking through the elements tree. Add it free.

Find it from the console

Paste this into DevTools with your sticky element’s selector. It walks the ancestor chain and reports every element that could be responsible:

(function (sel) {
  var el = document.querySelector(sel);
  if (!el) return console.warn('No element matches', sel);

  var cs = getComputedStyle(el);
  if (cs.position !== 'sticky')
    console.warn('Not sticky — position is', cs.position);
  if (['auto','auto auto'].includes([cs.top,cs.bottom,cs.left,cs.right].join(' ').trim())
      || (cs.top === 'auto' && cs.bottom === 'auto' && cs.left === 'auto' && cs.right === 'auto'))
    console.warn('No threshold set — add top: 0');

  var p = el.parentElement, found = 0;
  while (p) {
    var s = getComputedStyle(p);
    [['overflow', s.overflow], ['overflow-x', s.overflowX], ['overflow-y', s.overflowY]]
      .forEach(function (pair) {
        if (pair[1] && pair[1] !== 'visible' && pair[1] !== 'clip') {
          console.warn('Scroll container:', p, pair[0] + ': ' + pair[1]);
          found++;
        }
      });
    p = p.parentElement;
  }

  var parent = el.parentElement;
  if (parent && parent.getBoundingClientRect().height <= el.getBoundingClientRect().height + 1)
    console.warn('Parent has no spare height — nothing to travel through:', parent);

  if (!found) console.log('No blocking ancestor found. Check causes 3 to 5.');
})('.your-sticky-element');

2. No threshold was set

position: sticky on its own does nothing. The element needs to know where to stick, and with all four offsets at their initial auto, there is no threshold to cross — so it behaves like position: relative:

.header {
  position: sticky;
  top: 0;        /* required */
}

Any one of top, bottom, left or right works, and each pins a different edge. Note that top: 0 and bottom: 0 together do not center anything — the element sticks to whichever edge it reaches first.

3. The parent has no room

A sticky element can only move within its parent’s content box. When it reaches the parent’s bottom edge it stops and scrolls away with it. So if the parent is exactly as tall as the sticky element, there is no distance to travel and the sticky behaviour is invisible even though it is technically working.

This is the usual explanation for a sticky sidebar that scrolls off: the sidebar’s parent is only as tall as the sidebar. Give the parent the full column height — in a grid layout, the row track height — and it works:

.layout { display: grid; grid-template-columns: 260px minmax(0, 1fr); }
.sidebar { position: sticky; top: 20px; align-self: start; }

4. A flex or grid parent shrank it

Grid and flex items stretch to fill their track by default, which is usually what you want — but if something set align-items: center or flex-start on the container, the item becomes only as tall as its content, and you are back to cause 3. Adding align-self: start to the sticky element itself is the targeted fix, and it is worth adding by default on any sticky sidebar since it also prevents the item stretching to the full track height, which would leave it nothing to travel through.

5. Sticky on the wrong table element

For a sticky table header, put the sticky on the cells, not the row or the section:

thead th {
  position: sticky;
  top: 0;
  background: #fff;   /* required — rows show through otherwise */
}

Sticky on <tr> and <thead> has patchy support and does not work in Chromium. The background is not optional either: table cells are transparent by default, so a sticky header without one lets the scrolling rows render underneath it. The same applies to a sticky first column, where you also need a z-index so the column wins against the header.

Two more that catch people out

  • height: 100% on a wrapper. A chain of height: 100% elements ending in overflow: auto is a common app-shell pattern, and it means the page itself never scrolls — only that inner box does. Sticky elements outside it have nothing to react to.
  • A transformed ancestor. transform, filter, perspective or will-change on an ancestor creates a containing block for fixed and absolutely positioned descendants. It does not break sticky the way overflow does, but it changes what the offsets are measured against and can produce the same “it moved, but not where I expected” symptom. The same property set is what breaks z-index.

The debugging order

  1. Confirm the computed position really is sticky — a later rule or a shorthand may have overwritten it.
  2. Confirm a threshold is set. top: 0 is the usual one.
  3. Walk every ancestor to html looking for overflow that is not visible or clip. Use the console snippet above.
  4. Compare the parent’s height to the element’s height. Equal means no travel distance.
  5. If it is a table, check the sticky is on the cells.

Frequently asked questions

Why is position: sticky not working?

The most common cause by a wide margin is an ancestor with overflow set to hidden, auto or scroll. That element becomes the sticky element’s scroll container, and because it does not scroll, the threshold is never crossed. The next most common causes are a missing top or bottom value, and a parent that is no taller than the sticky element itself.

Does overflow: hidden break position: sticky?

Yes. Any overflow value other than visible and clip creates a new block formatting context and a scroll container, and sticky elements are confined to their nearest scroll container. hidden is scrollable programmatically even though it shows no scrollbars, which is why it counts. Replacing it with overflow: clip keeps the clipping and restores sticky.

What is the difference between overflow: hidden and overflow: clip?

Both hide overflowing content, but hidden makes the element a scroll container that can still be scrolled programmatically, while clip forbids all scrolling and creates neither a scroll container nor a new formatting context. That difference is why clip leaves descendant sticky elements working. The trade-off is that content inside a clip box cannot be scrolled into view by keyboard focus.

Why does overflow-x: hidden on body break sticky?

Because the overflow axes are not independent: when overflow-x is set to a value other than visible, overflow-y computes to auto rather than staying visible. The body therefore becomes a scroll container and every sticky descendant attaches to it instead of the viewport. Use overflow-x: clip instead, which does not have this effect.

Do I have to set top for sticky to work?

You need at least one of top, bottom, left or right. With all four at their initial value of auto there is no threshold to stick at, so the element behaves exactly like position: relative. top: 0 is the usual choice for a header.

Why does my sticky sidebar scroll away?

Because a sticky element only travels within its parent’s content box, and stops when it reaches the parent’s bottom edge. If the parent is only as tall as the sidebar, there is no travel distance. Give the parent the full height of the layout row — in grid, add align-self: start to the sticky element so it does not stretch to fill the track.

How do I make a sticky table header?

Apply position: sticky; top: 0 to the <th> cells rather than to <tr> or <thead>, which Chromium does not support. Give the cells an explicit background, since table cells are transparent by default and the scrolling rows would otherwise show through the header.

How do I find which ancestor is breaking sticky?

Walk the ancestor chain reading each element’s computed overflow, overflow-x and overflow-y, and flag anything that is not visible or clip. The console snippet in this article does that in one paste. Browser extensions such as CSS DNA show computed styles for an element and its ancestors together, which surfaces the same thing without scripting.

Does transform break position: sticky?

Not in the same way overflow does. A transform, filter, perspective or will-change on an ancestor establishes a containing block for positioned descendants, which changes what the offsets are measured against and can make a sticky element settle somewhere unexpected. If sticky is doing nothing at all, look for overflow first.

Read computed styles up the whole tree

See an element’s styles alongside every ancestor’s in one panel — so a stray overflow six levels up stops being a scavenger hunt.

Add CSS DNA to Chrome — Free