Fixing a Sticky Header That Stops Sticking Inside a Scrollable Container
A table header or section title set to position: sticky works perfectly in isolation, and then stops sticking the moment it's placed inside a card, a modal, or any container that itself scrolls or clips overflow. Nothing about the sticky element's own CSS changed; one of its ancestors has a property that silently takes away sticky positioning's ability to do anything at all.
The Problem
An element — a table header row, a section heading, a filter bar — is given position: sticky with a top offset, intended to stay pinned to the top of its scrolling context while the content beneath it scrolls past. Tested on its own, or inside a simple page, it works exactly as expected. Placed inside a card component, a modal, a flex or grid layout, or any container that scrolls independently, the same element simply scrolls away with everything else — sticky positioning appears to do nothing, with no error or warning anywhere to explain why.
Why It Happens
overflow: hidden, auto, or scroll on any ancestor creates a new scrolling context that can break the sticky chain
A sticky element sticks relative to its nearest scrolling ancestor — the closest parent with overflow set to something other than visible (or the viewport, if none exists). If an ancestor between the sticky element and its intended scrolling container has its own overflow set (even overflow: hidden used purely to clip a rounded corner or contain a float, with no scrolling actually intended), that ancestor becomes the sticky element's effective containing block — and if that container isn't actually the one that scrolls, the sticky behavior has nothing to stick relative to.
A parent with a defined height shorter than its sticky child's scrolling range prevents sticking from having room to occur
Sticky positioning needs the sticky element's containing block to be taller than the element itself — there has to be scrollable distance within that container for the element to visually "catch up to" and stick within. A parent sized to exactly fit its content, with no extra height for its children to scroll through, gives the sticky element no range to stick across, even though nothing about the CSS is technically wrong.
A flex or grid container can implicitly add properties that interfere with sticky behavior
Certain flex and grid layout behaviors — particularly around align-items: stretch (the default) interacting with how a sticky child's height is computed, or a grid item's implicit sizing — can produce a sticky element that technically has position: sticky applied but doesn't behave as expected because of how its box is sized or stretched within the flex/grid context.
This is specifically hard to debug because the browser gives no indication that sticky positioning silently failed
Unlike many CSS properties where an invalid value or unsupported feature is visible in DevTools with a strikethrough, a sticky element that isn't sticking due to an ancestor's overflow or sizing still shows position: sticky as a perfectly valid, applied style — there's no visual flag in the inspector pointing at the actual ancestor causing the problem, which has to be found by manually walking up the DOM tree.
The Fix
1. Walk up the DOM tree in DevTools checking every ancestor's overflow property
/* Check every ancestor between the sticky element and the
intended scrolling container for any of these: */
overflow: hidden;
overflow: auto;
overflow: scroll;
overflow-x: hidden; /* even just one axis is enough to break it */
Systematically inspecting each ancestor element's computed overflow value in DevTools — not just the immediate parent — finds the specific element that's unintentionally creating a competing scroll container, which is the most common root cause of this exact symptom.
2. Remove or relocate the interfering overflow property where the clipping isn't actually needed for scrolling
/* Instead of overflow: hidden on a card wrapper just to clip rounded corners: */
.card {
border-radius: 12px;
/* overflow: hidden; -- remove this if it's only here for the border-radius clip */
}
/* Use a different technique for clipping if genuinely needed, e.g. a mask
or restructuring so the clip and the scroll container aren't the same element */
Often the offending overflow was added for an unrelated reason (clipping a border radius, containing a float) rather than to create a scroll container — separating that concern from the actual scrolling container, or finding an alternative way to achieve the clip, removes the interference without sacrificing the original intent.
3. Ensure the sticky element's actual containing block has enough height for it to have room to stick
.scroll-container {
overflow-y: auto;
max-height: 500px; /* genuinely taller than the sticky header, with room for content to scroll beneath it */
}
.sticky-header {
position: sticky;
top: 0;
}
Confirming the intended scrolling container actually has a bounded height with content that exceeds it — rather than a container that happens to size exactly to its content — ensures there's genuine scrollable distance for the sticky element to remain pinned across.
4. Avoid align-items: stretch interactions by giving the sticky element explicit alignment within flex or grid parents
.flex-container {
display: flex;
align-items: flex-start; /* rather than the stretch default, which can affect sticky children */
}
Explicitly setting align-items (or the sticky child's own align-self) rather than relying on the flex default of stretch removes one specific, easy-to-overlook interaction between flex layout sizing and sticky positioning's own height requirements.
Why This Works
Each fix addresses a different way an ancestor can silently break the specific conditions sticky positioning depends on. Methodically checking every ancestor's overflow value finds the actual culprit rather than guessing; separating an unrelated overflow use from the real scroll container removes the interference at its source; ensuring the containing block has genuine scrollable height gives sticky positioning the room it structurally needs; and addressing flex/grid alignment defaults removes a less obvious but real interaction some layouts introduce.
Conclusion
A sticky element that stops sticking inside a container isn't a bug in the sticky element's own CSS — it's almost always an ancestor's overflow setting creating a competing scroll context, or a containing block that doesn't have enough height for sticking to have room to occur. Walk up the DOM tree checking every ancestor's overflow property, remove or relocate an overflow setting that was added for an unrelated reason like clipping, confirm the actual scroll container has genuine height beyond its content, and address flex or grid alignment defaults that can interact with how the sticky element's own box is sized.
