ScrollTrigger
ScrollTrigger is a GSAP plugin that ties any animation to scroll position. You give it a trigger element and a start/end range, and it controls when the tween plays, pauses, scrubs with the scrollbar, or pins the element in place. It's the engine behind almost every scroll-driven effect on modern brand sites.
Updated September 6, 2026
How ScrollTrigger works
Every ScrollTrigger has three parts: a TRIGGER element (the scroll-position reference), a START point, and an END point. As the user scrolls, the trigger's position relative to the viewport changes, and ScrollTrigger fires callbacks (or scrubs progress) within the start-to-end range.
The start / end syntax is "<trigger position> <viewport position>". "top 80%" means 'when the TOP of the trigger element crosses the 80% line of the viewport'. "bottom center" means 'when the BOTTOM of the trigger element crosses the viewport's vertical center'.
Free, and worth using responsibly
x, y, scale) rather than layout properties.Three behaviour modes - toggleActions, scrub, pin
ScrollTrigger has three primary behaviour modes that you mix per use case:
1. toggleActions
Fire-and-forget callbacks. The animation plays normally; ScrollTrigger just controls when. Use toggleActions: "play none none reverse" (the most common) to play on enter and reverse on leave-back.
2. scrub
Animation progress is bound to scroll position. The tween's time becomes the scroll position. Use for parallax, scroll-driven reveals, scroll-linked counters.
3. pin
The trigger element sticks in place while scroll progresses. Pin a hero, pin a section, pin a sidebar. Combine with scrub for the classic 'scroll inside this section animates content' pattern.
Reading start, end and markers
Every start and end value is two words: where on the trigger, then where in the viewport. The first word can also be a pixel or percentage offset on the trigger, and the second can be a pixel offset in the viewport.
start: 'top 80%'fires when the trigger's top reaches 80% down the viewport, the default reveal pointstart: 'top top'fires when the trigger's top touches the viewport top, the value you want before pinningend: 'bottom top'releases when the trigger's bottom leaves through the viewport topend: '+=500'ends 500 pixels of scroll after the start, the usual form for pinned scenesend: () => '+=' + el.offsetHeightcomputes the distance from the element, and stays correct on resize when paired withinvalidateOnRefresh: true
Turn on markers: true while building. GSAP draws the start and end lines for both the trigger and the scroller so you can see exactly where the tween is bound instead of guessing from the numbers. Remove it before shipping, or gate it behind a query flag.
Resize, late images, fonts and smooth scroll
ScrollTrigger measures every start and end once, then caches the pixel positions. Anything that changes page height after that measurement moves your triggers: images without a fixed height, fonts that swap in, an accordion opening above the section. Call ScrollTrigger.refresh() after layout settles, or give the images a width and height attribute so the layout never shifts in the first place.
With a smooth-scroll library such as Lenis, ScrollTrigger must be told about every scroll frame, and the two tickers must be one. Wire Lenis into GSAP's ticker and forward its scroll event; otherwise triggers fire a frame late and pinned sections stutter.
gsap was downloaded 18.7 million times from npm between 31 July and 29 August 2026 (api.npmjs.org). Most of those installs animate on scroll, and most scroll bugs reported in the GSAP forums trace back to one of three causes on this section: an unmeasured late layout shift, a smooth scroller that never called ScrollTrigger.update, or a trigger created before the plugin was registered.Use ScrollTrigger for
- Reveal animations as content enters the viewport
- Parallax effects
- Scroll-pinned sections with internal animations
- Progress indicators tied to scroll
- Anything where 'what's visible' needs to drive 'what's animating'
ScrollTrigger in React and Vue
In React, create ScrollTriggers inside useGSAP so they are killed when the component unmounts. Register the plugin in the same 'use client' file, and pass the component's ref as scope so selectors stay local. In Vue, do the same inside onMounted with gsap.context() and call ctx.revert() in onUnmounted.
Every scroll component in the Annnimate library ships this way: a vanilla version with an explicit kill(), a React version wrapped in useGSAP, and a Vue version wrapped in gsap.context(). See Image Dissolve Scroll and Mesh Gradient below for the shader-driven cases.
Use something else when
- Pure entrance animation on load - use a normal tween, no ScrollTrigger needed
- Element visibility detection without animation - use
IntersectionObserverdirectly (smaller, no GSAP dep) - Scroll-snap behaviour - native CSS
scroll-snapis better than ScrollTrigger - Smooth scrolling - that's Lenis or
ScrollSmoother, not ScrollTrigger
Used in these Annnimate components
- The Image Dissolve Scroll component scrubs a shader uniform 0→1 across the section's scroll range
- The Mesh Gradient shader uses ScrollTrigger to update color uniforms on scroll
See it running in production
Common questions
- Do I need to register ScrollTrigger before using it?
- Yes. Add
import { ScrollTrigger } from 'gsap/ScrollTrigger'andgsap.registerPlugin(ScrollTrigger)once at module load. In SSR contexts (Next.js), wrap the registration inif (typeof window !== 'undefined')so it only runs client-side. Forgetting to register is the #1 'why isn't my animation firing' cause. - What's the difference between scrub: true and scrub: 1?
scrub: trueties animation progress directly to scroll progress - instant, 1:1.scrub: 1adds a 1-second smoothing lag (the animation 'catches up' over 1 second), which feels more cinematic and forgiving on touchpad jitter. Most production sites use scrub values between 0.5 and 1.5.- Why does my pinned section break on resize?
- ScrollTrigger caches positions when triggers are created. If the layout changes (window resize, dynamic content), call
ScrollTrigger.refresh()to recalculate. Or setinvalidateOnRefresh: trueon individual triggers to also re-read the tween'sfromvalues. Pair with a debounced resize listener. - How do I use ScrollTrigger with Lenis smooth scroll?
- ScrollTrigger works with Lenis once Lenis drives GSAP's ticker and forwards its scroll event:
lenis.on('scroll', ScrollTrigger.update)plusgsap.ticker.add((t) => lenis.raf(t * 1000))andgsap.ticker.lagSmoothing(0). Without those two lines triggers fire a frame late and pins stutter. - Why do my ScrollTriggers fire at the wrong position after images load?
- ScrollTrigger caches start and end positions when it is created, so an image that loads later and pushes the page down moves every trigger below it. Give images explicit width and height attributes, or call
ScrollTrigger.refresh()afterwindowload anddocument.fonts.ready. - How do I make a ScrollTrigger fire once on enter and never again?
- Pass
once: trueinstead of (or alongside)toggleActions. This is the right pattern for entrance reveals where you don't want the animation to reverse when the user scrolls back up - common for hero text, image reveals, content tiles. - Can I use ScrollTrigger inside a Next.js Server Component?
- No - ScrollTrigger needs the DOM and window. Wrap your animated component in 'use client' and run the GSAP code inside useGSAP. The page itself can stay a Server Component; only the animated leaf needs to be client.
