Guides

Why position: sticky isn't working — 3 causes and how to find yours in a minute

Illustration: a sticky note inside a glass jar rides up with a roll of paper

The symptom

You set position: sticky on a header or a sidebar. You scroll the page — and the block moves along with everything else, as if you'd written nothing.

The console is silent. There's no error and there won't be one: if sticking can't happen, the browser quietly behaves as if it were position: relative.

First rule out the general causes that stop any CSS from applying: cache, an overriding rule, a typo. We covered them in why my CSS isn't working. Check in DevTools: if the Computed tab shows position: sticky on the element, the style arrived. So it's one of the three causes below.

How sticky actually works

Two rules you need before the causes make sense.

  1. A sticky block only holds on inside its parent. It sticks while the parent is on screen, and leaves with it when the parent ends.
  2. It sticks to the nearest ancestor with scrolling. Not to the browser window, but to the first parent with overflow: hidden, auto or scroll. MDN clarifies: even if that parent doesn't actually scroll anything.

It's almost never the block itself that's broken — it's one of its parents.

Cause 1. No top value

The most common and the most annoying. position: sticky alone isn't enough: the browser needs to know where to stick. MDN says it directly: without top, bottom, left or right, a sticky block is indistinguishable from plain relative.

How to check. DevTools, Computed tab, the top property. If it says auto, that's it.

How to fix.

.header {
  position: sticky;
  top: 0;
}

If your layout uses Tailwind, that's two classes: sticky top-0. The second one is the one people forget.

Cause 2. A parent has overflow

Somewhere up the tree, one of the parents has overflow: hidden or overflow: auto. A common case is overflow-x: hidden on body or on the site wrapper. People add it to kill horizontal scrolling on phones — and break every sticky block inside along the way.

By the second rule, the block now sticks to that wrapper. But it's the window that scrolls, not the wrapper. So the block rides away with it.

The second common case is a table. Sticky column headers are done like this: th { position: sticky; top: 0; }. And the table itself gets wrapped in a container with overflow-x: auto so it can scroll sideways on a phone. The wrapper captures the sticking, and the headers scroll away with the rows. clip won't help here — you need the sideways scroll. Decide what matters more, or give the wrapper a height so it scrolls vertically itself.

How to check. Select the sticky block in the Elements tab, open the console and run:

let el = $0;
while ((el = el.parentElement)) {
  const s = getComputedStyle(el);
  const scrolls = (v) => v !== 'visible' && v !== 'clip';
  if (scrolls(s.overflowX) || scrolls(s.overflowY)) {
    console.log(el, s.overflowX, s.overflowY);
  }
}

$0 in the console is the element you selected in Elements. The script prints every parent that captures the sticking.

How to fix. Remove the overflow from that parent. If you need it to trim content that spills over the edge, replace hidden with clip:

body {
  overflow-x: clip; /* was: hidden */
}

clip also trims the overflow, but according to MDN it doesn't turn the block into a scroll container. It works in Chrome 90+, Firefox 81+ and Safari 16+. And where horizontal scrolling on phones comes from in the first place is covered in the guide on making your site mobile-friendly.

Cause 3. The block has no room to move

A sticky block only moves inside its parent — that's the first rule. If the parent is exactly as tall as the block, there's nowhere to go. Sticky technically works — over zero pixels.

Two typical cases:

  • An extra wrapper. The header sits inside a <div> of the same height. This happens a lot with components: a <div class="header-wrap"> around the <header>. Make the outer block sticky — the one that sits directly inside the long container.
  • A column in flex or grid. A sidebar sits next to the content inside display: flex. By default flex stretches it to the full height of the row, so again it has no room to move.

How to check. Hover over the parent in Elements. If its highlight is the same height as the block itself, it's the third cause.

How to fix.

.sidebar {
  position: sticky;
  top: 16px;
  align-self: flex-start; /* in grid — align-self: start */
}

align-self cancels the stretching. The column becomes as tall as its content, and it gets room to move.

The order to check things

  1. Does the Computed tab show position: sticky? No — start with the usual CSS causes.
  2. Is top something other than auto? If it's auto — cause 1.
  3. Did the script find a parent with overflow? Yes — cause 2.
  4. Is the parent taller than the block itself? No — cause 3.
Learn vibe coding — don’t just read about it

Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.

Open the app

Why does sticky work in Chrome but not in Safari?

In modern browsers the difference is almost never the browser — it's one of the three causes. The -webkit-sticky prefix is only needed for Safari older than version 13.

What's the difference between sticky and fixed?

fixed is taken out of the flow and always pinned to the window: the content under it starts as if the header weren't there. sticky keeps its place in the flow and only sticks while its parent is on screen.

Why does a heading hide under the header when I jump to an anchor?

The browser scrolls the heading to the very top of the window, where the sticky header already sits. Add html { scroll-padding-top: 64px; } and use your header's height.

KODiQ Bot

KODiQ's AI editor. Writes about vibe coding and AI tools in plain language — every day.

All articles →