CSS position: sticky Explained
position: sticky toggles between relative and fixed based on scroll position within its containing block — no JavaScript scroll listener required.
position: sticky is a CSS positioning mode that treats an element as relative until it would scroll out of view, at which point it switches to fixed behavior and sticks in place — but only within the bounds of its containing block. Once the parent scrolls past, the sticky element scrolls away with it. It’s the CSS-native way to build a table header that stays visible while you scroll through rows, or a section label that stays pinned while its content scrolls beneath it, without writing a scroll event handler.
How it actually behaves
A sticky element needs two things to work: a threshold and a containing block with room to move.
.sticky-header {
position: sticky;
top: 0;
}
That top: 0 is the threshold — it tells the browser “stick when the element’s top edge would otherwise scroll past the top of the viewport (or scrolling ancestor).” As the page scrolls, the element behaves normally (relative) until its edge hits that threshold. From then on, it behaves like fixed, staying pinned at top: 0 — but only while its parent container is still on screen. Once the parent’s bottom edge scrolls past, the sticky element scrolls away with it, because it was never actually removed from the document flow.
This is the detail that trips people up most: sticky positioning is scoped to the containing block, not the viewport. A fixed element floats relative to the viewport indefinitely. A sticky element only floats relative to the viewport for as long as its parent is in view.
Why sticky elements sometimes don’t stick
A handful of conditions silently disable sticky positioning, and none of them throw an error — the element just behaves as if position were unset:
- No threshold set. Without at least one of
top,bottom,left, orright, the browser has nothing to stick to. - An ancestor with
overflow: hidden,scroll, orauto. Sticky positioning is calculated relative to the nearest scrolling ancestor. If any ancestor between the element and that scroll container clips overflow, the sticking calculation can break. - A parent with a fixed, small height. If the containing block is only as tall as the sticky element itself, there’s no room for the element to “become” sticky — it never gets a chance to detach from its starting position.
overflowset on a parent that isn’t the scroll container. This is the most common real-world cause: aoverflow-x: autoadded to fix a horizontal scroll issue elsewhere in the layout quietly breaks sticky headers nested inside it.
If a sticky element isn’t sticking, check every ancestor for an overflow value other than visible before assuming the CSS itself is wrong.
Sticky vs the other position values
| Value | Removed from flow? | Reference point | Common use |
|---|---|---|---|
static | No | Normal document flow | Default for everything |
relative | No | Its own normal position | Small nudges, anchor for absolute children |
absolute | Yes | Nearest positioned ancestor | Tooltips, dropdowns, overlays |
fixed | Yes | Viewport | Persistent nav bars, modals |
sticky | No (until threshold) | Its containing block, then viewport-like | Section headers, sticky table headers, in-page nav |
Sticky is the odd one out because it’s the only value whose behavior changes mid-scroll — it starts in flow like relative and switches to viewport-anchored like fixed once the threshold is crossed. Understanding the CSS box model helps here too, since a sticky element still occupies its original space in the layout even while visually pinned elsewhere, which is different from how absolute and fixed remove an element from flow entirely.
Practical patterns
Sticky table headers. Apply position: sticky; top: 0; to <thead> or its cells so column headers stay visible while scrolling long tables — a common pairing with data-heavy dashboards.
Section navigation. A sidebar of section links with position: sticky; top: 1rem; stays in view as the main content scrolls, without the layout jump that switching to fixed at a scroll threshold used to require in JavaScript-based implementations.
Sticky footers within a scrollable panel. Combine position: sticky; bottom: 0; on an action bar inside a scrollable modal or panel, so buttons stay reachable regardless of content length.
Sticky positioning also interacts with stacking contexts the same way relative and fixed do — once an element is visually detached from flow, its z-index starts mattering relative to siblings and ancestors, so a sticky header can end up behind or in front of other content depending on how stacking contexts are structured elsewhere on the page.
Sticky and layout systems
Sticky positioning composes cleanly with both Flexbox and Grid layouts — it doesn’t require either, and it doesn’t conflict with flex-grow, flex-shrink, and flex-basis sizing rules on the sticky element’s siblings. The one thing to watch is that a flex or grid container with overflow set (even implicitly, through certain layout combinations) can trip the same containing-block issue described above.
If you’re pairing sticky elements with responsive breakpoints, @media queries are still the right tool for changing thresholds or disabling sticky behavior on small screens where a persistently pinned header eats too much vertical space. And for scroll-snapping sections combined with sticky navigation, see CSS scroll snap for how the two interact.
The takeaway
position: sticky gives you a scroll-aware element that behaves like relative until a threshold is crossed, then behaves like fixed — but only within its containing block, not the whole page. Set a threshold (top, bottom, left, or right), give the parent enough room to move, and check every ancestor for a stray overflow value if it isn’t sticking. It replaces a large share of what used to require scroll event listeners, with better performance and no JavaScript at all.
Tagged
Keep reading
Takina · · 4 min read CSS @starting-style Explained: Entry Transitions in CSS
CSS @starting-style defines an element's before-state on first render, so transitions can animate elements as they appear, even from display: none.
Takina · · 4 min read Container Query Units: cqw, cqh, and cqi Explained
Container query units like cqw and cqi size elements relative to their containing element, not the viewport — here's how and when to use them.
Takina · · 4 min read CSS Relative Color Syntax: Deriving Colors from Colors
Relative color syntax lets CSS derive a new color from an existing one by adjusting its channels directly, no preprocessor or JS required.