position: sticky doesn't work.
Three causes, three fixes.
You added position: sticky and top: 0. The element should stick to the top when scrolling. It doesn't. Or it sticks inside a tiny box and scrolls away immediately. Or nothing happens at all. There are three causes — the missing offset, an overflow ancestor, and a parent that isn't tall enough. This page covers each, with the exact fix.
Short answer: Sticky needs three things. (1) An offset — top, bottom, left, or right — not just position: sticky. (2) No scrolling ancestor — no overflow: hidden/auto/scroll on any parent. (3) A parent tall enough to scroll past — min-height on the parent, or content that exceeds the viewport. Fix all three and sticky works. Skip any one and it behaves like position: relative.
✅ Cause 1 — Missing the offset
position: sticky alone does nothing.
.sidebar {
position: sticky;
/* ❌ no top, no bottom, no left, no right /
background: white;
}
❌ No offset — the element has no threshold. ❌ Behaves like position: relative. ❌ No sticking, no visual change. sticky needs at least one offset: top, bottom, left, or right The offset is the threshold — how close the element can get to the viewport edge before it starts sticking. Without it, sticky is a no-op.
.sidebar {
position: sticky;
top: 1rem; /* ✅ sticks 1rem from viewport top */
background: white;
padding: 1rem;
}
/* Or with a sticky header: /
.header {
position: sticky;
top: 0;
z-index: 10; / keep it above the page /
background: white;
}
✅ top: 1rem → sticks 16px from the top. ✅ top: 0 → sticks flush with the viewport top. ✅ z-index: 10 → stays above scrolling content. ⚠️ Always set a background on the sticky element. Without one, the underlying content shows through and the element looks broken. ⚠️ z-index needs a positioned context. position: sticky creates one — you're fine. But siblings without stacking context may paint over it. z-index: 10 is a safe default.
✅ Cause 2 — An overflow ancestor
The trickiest cause: a parent scrolls.
/* ❌ The parent has overflow: hidden */
.page-wrapper {
overflow: hidden;
}
.sticky-sidebar {
position: sticky;
top: 1rem;
/* Doesn't stick to viewport — sticks to .page-wrapper,
which doesn't scroll. So no visual effect. */
}
/* ✅ Fix 1: remove overflow from the ancestor /
.page-wrapper {
/ overflow: hidden — removed */
}
.sticky-sidebar {
position: sticky;
top: 1rem;
}
/* ✅ Fix 2: move sticky outside the overflow */
.page-wrapper {
overflow: hidden;
}
/* The sticky element is a sibling of the wrapper,
not a child of it. /
.sticky-header {
position: sticky;
top: 0;
}
sticky positions an element relative to its nearest scrolling ancestor. overflow: hidden / auto / scroll on a parent makes that parent the scrolling container. If that parent doesn't scroll (or doesn't scroll vertically), sticky has nowhere to stick. Common overflow ancestors: ❌ A layout wrapper with overflow: hidden ❌ A card with overflow: auto ❌ A table cell with overflow: scroll ❌ A parent with overflow-x: auto Fix: → Remove overflow from the ancestor → Or restructure so sticky element is a sibling of the overflow container This is the #1 reason sticky "doesn't work" after the missing-offset case.
Don't add overflow: hidden to a parent to "hide horizontal scroll." This is the most common cause of sticky not working — a developer adds overflow-x: hidden on body or a wrapper to hide an accidental horizontal scroll, and every sticky element on the page stops working. The overflow becomes the scrolling container, and sticky sticks to it instead of the viewport. Fix the underlying overflow (find the element that's too wide) rather than hiding it. If you must hide overflow, apply it as low in the DOM as possible — not on body or the layout wrapper.
✅ Cause 3 — Parent isn't tall enough
Sticky stops at the parent's edge.
/* ❌ The parent is only as tall as the sidebar /
.sidebar-wrapper {
/ height: auto — the sidebar's own height */
}
.sticky-sidebar {
position: sticky;
top: 1rem;
/* No room to stick — the wrapper ends where
the sidebar begins. */
}
/* ✅ Fix: parent taller than the sticky element /
.content-layout {
display: grid;
grid-template-columns: 240px 1fr;
align-items: start; / don't stretch the sidebar */
}
.sticky-sidebar {
position: sticky;
top: 1rem;
}
.content-layout .main {
/* The main column is tall — this makes the
grid parent tall. The sidebar sticks
within the tall parent. /
}
sticky is constrained to its containing block — the nearest positioned ancestor. The sticky element sticks within the parent's bounds. If the parent is only as tall as the sticky element, there's no scroll range to stick within. Rules: ✅ Parent must be taller than the sticky element. ✅ For sticky sidebar: parent is the layout container, and it's as tall as the main content. ✅ align-items: start on the grid/flex parent prevents the sidebar from stretching (which would collapse the parent back to the sidebar's height). ⚠️ Don't set height: 100vh on the sticky element. That pins it to its own height, not the parent. ⚠️ Don't set height: auto on the parent and expect sticky to stick — it'll only stick within the parent's natural height.
⚠️ Common mistakes
The position:sticky mistakes that cost real time.
position: sticky without top. Fix: add top: 0 or the appropriate direction.
overflow: hidden on a parent makes it the scroll container. Fix: remove it.
The sidebar's parent is only as tall as the sidebar. Fix: make the parent the full layout container.
Content shows through. Fix: set a background on the sticky element.
Sticky element hides under siblings. Fix: add z-index: 10 (or higher).
Flex/grid item stretches, collapsing the scroll range. Fix: align-items: start.
fixed stays forever, sticky scrolls away. Fix: pick the right one for the effect.
It sticks to the inner scroll container, not the viewport. Fix: understand the containing block.
❓ Frequently asked
Why isn't position: sticky working?
Three causes. (1) Missing a top, bottom, left, or right offset — sticky requires a direction. (2) An ancestor has overflow: hidden, overflow: auto, or overflow: scroll — sticky only works if no ancestor scrolls horizontally or vertically except the viewport. (3) The parent doesn't have enough height — sticky elements stop scrolling at the parent's edge, so a parent that's exactly as tall as its content leaves no room to stick.
Does position: sticky need a top value?
Yes. sticky needs at least one of top, bottom, left, or right to define the threshold. top: 0 is the most common — the element sticks to the top of the viewport once it scrolls past. Without an offset, the element is sticky in theory but has nothing to stick against, so it behaves like position: relative. This is the most common cause of 'sticky doesn't work'.
Why does overflow: hidden break position: sticky?
sticky positions an element relative to the nearest scrolling ancestor. If an ancestor has overflow: hidden, overflow: auto, or overflow: scroll, that ancestor becomes the scrolling container — not the viewport. The sticky element sticks to that container's edges instead of the viewport's. If the container isn't tall enough to scroll, or doesn't scroll vertically, the element appears not to stick.
Does position: sticky work inside flexbox or grid?
Yes, but with a caveat. Sticky works on flex items and grid items — the item sticks relative to the scroll container. However, sticky positioning inside a flex container can be constrained by the flex item's height. If the flex item is stretched to match the parent's height (default align-items: stretch), the sticky element inside it has no room to move. Fix: set align-items: start on the flex container.
Why does position: sticky stop at the end of its parent?
Because sticky is constrained to its containing block — the nearest ancestor that isn't static positioned. The element sticks within the parent's bounds. Once the parent scrolls out of view, the sticky element goes with it. To make the element stick for the full page, the parent must be tall enough to contain the full scroll range.
What is the difference between position: sticky and position: fixed?
fixed pins the element to the viewport — it stays in place regardless of scroll, always visible. sticky pins the element to the viewport only while scrolling within its parent's bounds — it becomes fixed at a threshold, then scrolls away with the parent. fixed removes the element from flow; sticky keeps it in flow.