Problem Solved

CSS · position sticky

Home › Courses › CSS › Problems › Position sticky
🎨 Concept · 3 min read

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.

no-offset.css
.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.
with-offset.css
.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.

overflow-breaks.css
/* ❌ 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.

short-parent.css
/* ❌ 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.

1. No offset

position: sticky without top. Fix: add top: 0 or the appropriate direction.

2. overflow on an ancestor

overflow: hidden on a parent makes it the scroll container. Fix: remove it.

3. Parent too short

The sidebar's parent is only as tall as the sidebar. Fix: make the parent the full layout container.

4. No background

Content shows through. Fix: set a background on the sticky element.

5. z-index not set

Sticky element hides under siblings. Fix: add z-index: 10 (or higher).

6. align-items: stretch on the parent

Flex/grid item stretches, collapsing the scroll range. Fix: align-items: start.

7. Confusing with position: fixed

fixed stays forever, sticky scrolls away. Fix: pick the right one for the effect.

8. Sticky inside overflow:auto on purpose

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.

🔗 Related

← All CSS problems
Problem solved · CSS