Skip to content
DevDepth
← Back to all articles

In-Depth Article

Why `position: sticky` Is Not Working and How to Fix It

Learn why `position: sticky` fails when thresholds are missing, ancestors use overflow, or Flexbox and Grid stretch the sticky item so it has nowhere to travel.

Published: Updated: 6 min readlayout-strategy

position: sticky is one of the most useful CSS layout features because it can create sticky headers, sticky sidebars, and scroll-linked section labels without JavaScript.

It is also one of the easiest CSS features to mistrust.

You add:

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

and sometimes it works perfectly. Other times it does nothing, or it only sticks inside a tiny area, or it stops working the moment the element enters a Flexbox or Grid layout.

That is not random. Sticky positioning is very sensitive to ancestor overflow, available travel space, and the layout context around it.

This guide focuses on the real failure modes developers hit most often:

  • no sticky threshold was defined
  • an ancestor became the wrong scroll container
  • the sticky item has no room to move
  • Flexbox or Grid stretch behavior makes the sticky item too tall
  • the HTML structure itself works against the sticky pattern

If you want the broader layout baseline first, start with Fix Scroll and Stretch Issues in Flexbox and Grid. If you want the direction-safe version of offset properties such as top and left, continue with RTL, LTR, and CSS Logical Properties: A Practical Layout Guide.

1. A very short position refresher

You do not need a full positioning course to debug sticky, but one distinction matters:

  • static: normal flow, no positioning behavior
  • relative: still in normal flow, but visually offset from its own original position
  • absolute: removed from normal flow and positioned against a containing block
  • fixed: removed from normal flow and usually positioned against the viewport
  • sticky: behaves like relative positioning until a threshold is reached, then behaves more like a fixed element within a limited region

That last phrase is the key:

sticky does not pin itself to the viewport forever. It sticks within a constrained range.

2. How sticky actually works

A sticky element needs three things to behave visibly:

  1. a sticky threshold such as top: 0
  2. a scroll context it can react to
  3. enough space inside its containing block to move

That means sticky is always a relationship between:

  • the sticky item
  • the ancestor scroll mechanics around it
  • the container that limits how far it can travel

When any one of those pieces is wrong, sticky looks broken even though the browser is following the rules.

3. Failure mode one: no threshold means nothing ever sticks

This is the simplest bug:

.header {
  position: sticky;
}

Without an inset threshold, the browser has no trigger for the sticky behavior.

The fix is to set at least one relevant inset value:

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

Or, using logical properties:

.header {
  position: sticky;
  inset-block-start: 0;
}

Use the threshold that matches the scrolling axis:

  • vertical sticky headers usually need top or inset-block-start
  • bottom-pinned sticky UI usually needs bottom or inset-block-end
  • horizontal sticky UI needs left or right, or their logical equivalents

If you skip this step, sticky behaves much more like position: relative.

4. Failure mode two: an ancestor with overflow becomes the wrong scroll container

This is the sticky bug that surprises people most often.

If a nearby ancestor has:

  • overflow: hidden
  • overflow: auto
  • overflow: scroll
  • or any overflow combination that creates a scrolling mechanism

then the sticky element will stick relative to that ancestor instead of the viewport-level scrolling you may have expected.

That is why a wrapper like this can quietly change everything:

<div class="container">
  <div class="sticky">Sticky Element</div>
</div>
.container {
  overflow: hidden;
}

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

The sticky item is now constrained by that overflow ancestor.

Fix option one: if you only need clipping, prefer overflow: clip

If the real goal is "hide overflow" rather than "create a scroll container," this is often safer:

.container {
  overflow: clip;
}

clip cuts off the overflow visually without turning the element into the same kind of scrolling mechanism that commonly interferes with sticky behavior.

Fix option two: if the overflow container is intentional, give it a real size

Sometimes the scroll container is exactly what you want. In that case, the sticky element should stick within that container's scroll area, so the container needs a real height contract:

.container {
  block-size: 100dvh;
  overflow: auto;
}

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

That tells the browser:

  • this container is the scroll region
  • the sticky item should react inside it

Without that size contract, the overflow container may not produce the behavior you expect.

5. Failure mode three: the sticky item has nowhere to travel

Sticky needs room to move.

If the sticky item's containing block is only as tall as the sticky item itself, the sticky behavior may technically exist but never become visible in a useful way.

That is why this pattern often fails:

<div class="container">
  <div class="sticky">Sticky Element</div>
</div>
<main>Long content below</main>

The page scrolls, but the sticky element is trapped inside a short wrapper that gives it no meaningful travel space.

The fix is structural:

  • make sure the sticky item lives inside the section whose scroll range it should track
  • make sure that section is tall enough to create room for sticky behavior

This is the same reason sticky section headings work best when each heading belongs to its own section block rather than all headings sharing one flat container.

6. Failure mode four: Flexbox and Grid stretch the sticky item

This is the layout-specific version of the previous bug.

A sticky sidebar often fails inside Flexbox or Grid because the layout stretches the sidebar to match the height of the taller content column. Once the sticky item becomes as tall as the container, it has no real room to stick.

Flexbox example

.layout {
  display: flex;
  gap: 1.5rem;
}

.main {
  flex: 1 1 0;
}

.sidebar {
  inline-size: 20rem;
  position: sticky;
  top: 2rem;
}

By default, the row flex container uses align-items: stretch, which can make the sidebar as tall as the row itself.

The fix is to stop the stretch:

.layout {
  display: flex;
  gap: 1.5rem;
  align-items: flex-start;
}

Or on the sticky item itself:

.sidebar {
  position: sticky;
  top: 2rem;
  align-self: flex-start;
}

Grid example

The same issue appears in Grid:

.layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 20rem;
  gap: 1.5rem;
}

.sidebar {
  position: sticky;
  top: 2rem;
}

The usual fix is:

.layout {
  align-items: start;
}

or:

.sidebar {
  position: sticky;
  top: 2rem;
  align-self: start;
}

This is the same alignment rule discussed in Fix Scroll and Stretch Issues in Flexbox and Grid: stretch is often the quiet reason a component stops behaving the way you expected.

7. Section-based sticky headings need the right HTML structure

Sticky index or section-label layouts are a great example of how markup and sticky behavior interact.

This structure works well:

<section>
  <h3>Section Title</h3>
  <div>Section Content</div>
</section>

<section>
  <h3>Section Title</h3>
  <div>Section Content</div>
</section>
section {
  display: grid;
  grid-template-columns: 12.5rem minmax(0, 1fr);
  gap: 2rem;
}

section h3 {
  position: sticky;
  top: 8rem;
  align-self: start;
}

Why this works:

  • each heading is constrained by its own section
  • each section creates a natural sticky range
  • the next heading can replace the previous one cleanly as the page scrolls

A flatter structure often behaves much worse:

<section>
  <h3>Title</h3>
  <div>Content</div>
  <h3>Title</h3>
  <div>Content</div>
</section>

In that arrangement, multiple sticky headings may stack or overlap in ways that do not match the intended index pattern.

Sticky is not only about CSS. The document structure has to support the range you want.

8. A fast sticky debugging checklist

When sticky is not working, check these in order:

  1. Did I set a threshold such as top, bottom, or inset-block-start?
  2. Does an ancestor with overflow now own the scrolling behavior?
  3. If that overflow ancestor is intentional, does it have a real height or block-size?
  4. Does the sticky item have room to travel inside its containing block?
  5. Is Flexbox or Grid stretching the sticky item to the same height as the container?
  6. Does the HTML structure actually give each sticky item its own useful sticky range?

This sequence usually finds the cause faster than toggling random properties in DevTools.

9. The takeaway

position: sticky is not fragile because the feature is bad. It is fragile because the rules around it are easy to forget:

  • sticky needs a threshold
  • sticky reacts to scroll containers, not just the page
  • sticky is constrained by its available range
  • Flexbox and Grid can accidentally remove that range through stretch behavior
  • structure matters, especially for sticky headings and sticky sidebars

The most reliable fixes are usually small:

  • add top, bottom, or logical inset values
  • avoid unnecessary overflow ancestors
  • use overflow: clip when you only need clipping
  • give intentional scroll containers a real size
  • reset align-items or align-self when stretch is the problem

Once you learn to check those layers in order, sticky stops feeling magical and starts feeling predictable.

Reviewed by

DevDepth Editor

Editor and frontend engineering writer

DevDepth publishes practical guides on React, Next.js, TypeScript, frontend architecture, browser APIs, and performance optimization.

Each article should be reviewed for technical accuracy, code clarity, metadata quality, and internal-link fit before it goes live.

Last editorial review: 2026-03-17

Contact the editor