sticky-pinning-sequences
Sticky / Pinning Sequences
When to use this
- A single element (image, chart, sidebar nav) should stick in place while its sibling content scrolls, and you don't need scrubbed animation tied to the pin duration: use plain CSS
position: sticky. - A pinned element needs to visually change (fade, morph, reveal text) in sync with how far the user has scrolled through the pinned duration, not just sit there: this needs GSAP ScrollTrigger's
pinoption, since nativestickyhas no progress signal. - You need a pin-then-release sequence: element pins, something happens, then it releases and scrolls away normally to make room for the next pinned section.
- You have nested or stacked pins (a pinned parent section containing its own pinned sub-elements) and need to reason about pin-spacing so later content does not overlap or leave dead gaps.
- Do NOT use this for horizontal-scroll-driven-by-vertical-input galleries; that is a specific pin variant covered in
horizontal-scroll-gallery. Do NOT use GSAP pinning when plain CSS sticky achieves the same visual result with no JS; reach for GSAP only when you need scroll-progress-driven property changes during the pin.
Mental model
position: sticky is a layout mode, not a scroll listener: the browser itself decides, purely from CSS box positioning, when the element switches from "in normal flow" to "stuck at its top/bottom offset relative to its nearest scrolling ancestor," and switches back once its containing block's edge scrolls past. It is compositor-cheap and requires zero JS, but gives you no hook into "how far through being stuck are we" — you only get a binary stuck/unstuck state (detectable via IntersectionObserver on a sentinel element if you need to know, but there's no built-in progress).
GSAP ScrollTrigger's pin: true fakes the same visual result differently: it takes the element out of flow, sets it to position: fixed (or absolute depending on context) for the trigger's active duration, and crucially inserts a spacer element into the document with height equal to the pin's scroll duration, so the rest of the page's layout does not collapse when the pinned element stops taking up flow space. This spacer (pin-spacing) is the single most important, most misunderstood mechanic in ScrollTrigger pinning: the pin's end value determines how tall that spacer is, and if you get end wrong, either later content jumps up awkwardly (spacer too short) or there's a dead unscrollable gap (spacer too tall, nothing animating during a leftover chunk of pinned scroll).
Because ScrollTrigger's pin gives you self.progress (0 to 1 across the pin's active scroll range), you can drive any property change in lockstep with how far the user has scrolled through the pin, which is what makes multi-stage "pin, then reveal three things one after another, then release" sequences possible; native sticky cannot do this without pairing it with a separate scroll-timeline (see css-scroll-driven-animations) for the progress-driven part.
Nested pins (a pin inside another pinned container) work but stack spacers: the outer pin's spacer must already account for the inner pin's full duration, or the outer section releases before the inner sequence finishes. Always build and measure the innermost pin's total scroll distance first, then set the outer pin's end to be at least that long.