Scroll

Pinning

Pinning is a ScrollTrigger feature that locks an element in place while scroll progresses past it. The element appears to stay still while content scrolls underneath, until ScrollTrigger reaches the `end` position and releases. It's the engine behind sticky hero scenes, scroll-driven case studies, and the `pin a section, scrub content inside it` pattern.

Updated June 1, 2026

Mechanics

How pinning works

Set pin: true on a ScrollTrigger and the trigger element holds its position from start to end. ScrollTrigger inserts a placeholder of the same height into the document flow so nothing else jumps, then sets the element to position: fixed for the pin duration. When scroll exits the range, the element returns to its normal flow.

script.js

The combination above is the classic horizontal-scroll-inside-a-pinned-section pattern. Pin holds the scene; scrub binds the inner tween to the same 2000px scroll range.

pinSpacing

Reserving scroll space

By default ScrollTrigger adds pinSpacing (a placeholder) so the layout below the pinned element doesn't jump. Set pinSpacing: false only when the pinned element is position: absolute or otherwise removed from flow, or when you intentionally want surrounding content to scroll under the pinned one without reserving space.

Common bug

If your pinned section's height looks doubled in DevTools, pinSpacing: true is doing exactly what it should - reserving the scroll distance. The pinned element renders at its normal height, plus a placeholder spacer matching the pin duration.
When

Use pinning for

  • Hero scenes that hold while a tagline scrubs in
  • Horizontal scroll sections inside a vertical page
  • Case-study sections where the visual holds while copy advances
  • Side-by-side reveals where one side pins and the other scrolls past
  • Long-form data stories with a sticky visualization and scrolling captions
Alternatives

Use something else when

  • Sticky navigation or sidebar - native CSS position: sticky is lighter and doesn't need GSAP
  • Short reveals on enter - regular ScrollTrigger without pin is enough
  • Mobile (touch devices) where pinning often clashes with momentum scroll - gate via gsap.matchMedia and ship a non-pinned variant under (max-width: 768px)
In production

Used in these Annnimate components

  • The Parallax component pins the scene wrapper and scrubs layer transforms across the pin range
  • The Multi Flip component pins the gallery and orchestrates a sequence of flip cards inside the pinned range
  • The Popping Text component pins the headline and stagger-reveals characters as scroll progresses
  • The Image Dissolve Scroll shader pins the canvas and scrubs the dissolve uniform 0->1 across the pin
Used in components

See it running in production

FAQ

Common questions

Why does my pinned section jump on resize?
ScrollTrigger caches the pin's start/end positions at creation. After a resize the layout shifts, but the cached values don't. Call ScrollTrigger.refresh() after the resize settles (debounced) so it recalculates. For pinned sections with content that may reflow, set invalidateOnRefresh: true on the trigger.
Should I use pinType: 'fixed' or 'transform'?
Default is pinType: 'fixed' which uses position: fixed. Switch to pinType: 'transform' when the pinned element is inside a scrollable ancestor that isn't the window (e.g. an inner overflow-auto div). fixed ignores the inner scroller and visually breaks; transform respects it.
How do I pin only on desktop?
Wrap the ScrollTrigger creation in gsap.matchMedia() with the breakpoint you want. Inside the desktop branch, create the ScrollTrigger with pin: true; inside the mobile branch, omit the pin (or skip the trigger entirely). matchMedia auto-reverts the desktop branch on a resize down to mobile.
Can I pin multiple sections in a row?
Yes - each section gets its own ScrollTrigger with its own start, end, and pin: true. Just confirm the pinSpacing on each is correct so the natural document height accounts for all pin ranges. Two sections pinning for 2000px each add 4000px of scroll distance to the page.
Why does pinning break inside Annnimate's auth'd (app) chrome?
The (app) tree uses native scroll on an inner overflow-auto div, not on window. Pin defaults to position: fixed against the viewport, which ignores the inner scroller. Embed pinned components only on marketing/public surfaces, or set pinType: 'transform' and confirm the ScrollTrigger is wired to the inner scroller via scrollerProxy. See the scroll architecture rule.