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.
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 behaviorrelative: still in normal flow, but visually offset from its own original positionabsolute: removed from normal flow and positioned against a containing blockfixed: removed from normal flow and usually positioned against the viewportsticky: 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:
- a sticky threshold such as
top: 0 - a scroll context it can react to
- 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
toporinset-block-start - bottom-pinned sticky UI usually needs
bottomorinset-block-end - horizontal sticky UI needs
leftorright, 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: hiddenoverflow: autooverflow: 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:
- Did I set a threshold such as
top,bottom, orinset-block-start? - Does an ancestor with
overflownow own the scrolling behavior? - If that overflow ancestor is intentional, does it have a real height or block-size?
- Does the sticky item have room to travel inside its containing block?
- Is Flexbox or Grid stretching the sticky item to the same height as the container?
- 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: clipwhen you only need clipping - give intentional scroll containers a real size
- reset
align-itemsoralign-selfwhen 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