GSAP ScrollTrigger Explained — Complete Guide to Scroll-Driven Animations
%%{init: {"flowchart": {"htmlLabels": true}} }%%
flowchart LR
A["Getting Started"] --> B["Timelines & Sequencing"]
B --> C["You Are Here: ScrollTrigger"]
C --> D["Easing & Motion Paths"]
C --> E["Performance & Best Practices"]
click A "/frontend/libraries/gsap/gsap-getting-started"
click B "/frontend/libraries/gsap/gsap-timelines-sequencing"
click C "/frontend/libraries/gsap/gsap-scrolltrigger"
click D "/frontend/libraries/gsap/gsap-easing-motion"
click E "/frontend/libraries/gsap/gsap-performance"
GSAP ScrollTrigger links animation progress directly to the scroll position — creating fade-ins, parallax layers, pinned sections, and scrub-tied effects without writing a single scroll event listener.
In this tutorial, you’ll learn how ScrollTrigger transforms a static page into a narrative experience, with practical examples you can use in your own projects. These are the same techniques that power the onboarding flows in Doda Browser and the scan result transitions in Durga Antivirus Pro.
What You’ll Learn
- Install and register ScrollTrigger via CDN or npm
- Create basic scroll-triggered fade-in animations
- Use
ScrollTrigger.create()for full control - Master start/end position values
- Pin elements in place during scroll
- Scrub animations so they follow the scrollbar bidirectionally
- Build parallax depth effects
- Create horizontal scrolling sections
- Make animations responsive with
matchMedia() - Debug with markers and refresh properly
Why ScrollTrigger Matters
Before ScrollTrigger, scroll-linked animations meant writing manual scroll event listeners, calculating positions with getBoundingClientRect(), and managing throttling. It was error-prone and hard to maintain.
Security note: Understanding Gsap Scrolltrigger helps build more secure applications — a core principle at DodaTech, where tools like Durga Antivirus Pro and Doda Browser rely on solid implementation practices.
ScrollTrigger does all that work for you. You declare what should happen when, and the plugin handles the math. This means:
- Better performance — uses GSAP’s ticker, not raw scroll events
- Precise control — millisecond-accurate trigger points
- Bidirectional scrubbing — scroll forward = play, scroll back = reverse
- Pinning — lock elements in place during animation
Think of the scrolling dashboard in DodaZIP’s file manager — sections pin, progress bars fill, cards fade in. All powered by ScrollTrigger.
Prerequisites
Installation — Adding ScrollTrigger
CDN
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script>
gsap.registerPlugin(ScrollTrigger); // ← MUST call this
</script>npm
npm install gsapimport gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger); // ← MUST call this
Critical: If you forget gsap.registerPlugin(ScrollTrigger), the plugin silently fails. Nothing works and there’s no error. Always check this first when debugging.
Basic ScrollTrigger — Fade In on Scroll
The simplest use case: animate an element when it scrolls into view.
gsap.from(".card", {
scrollTrigger: {
trigger: ".card", // the element that triggers the animation
start: "top 80%", // when .card's top hits 80% from viewport top
end: "top 30%",
toggleActions: "play none none none" // play once on enter
},
y: 60,
opacity: 0,
duration: 0.8
});What happens step by step:
- The user scrolls down
- When
.card’s top edge reaches 80% of the viewport height, GSAP starts the animation - The card slides up 60px and fades in over 0.8 seconds
- Since
toggleActionsis"play none none none", it plays once and ignores further scroll
ScrollTrigger.create() — Full Control
For advanced configurations, use ScrollTrigger.create(). This is the verbose form that exposes every option.
ScrollTrigger.create({
trigger: ".section",
start: "top center",
end: "bottom center",
onEnter: () => console.log("Entered"),
onLeave: () => console.log("Left"),
onEnterBack: () => console.log("Entered from below"),
onLeaveBack: () => console.log("Left from above")
});When to use this: When you need callbacks, markers, or complex pinning without a paired tween.
Start & End Values — The Position System
The start and end values use the format "{trigger-edge} {viewport-edge}".
// Trigger when element's top hits 80% from viewport top
start: "top 80%"
// End when element's bottom hits 20% from viewport top
end: "bottom 20%"
// Absolute pixel offsets
start: "top -=100px"
// Relative to trigger's own height
start: "top+=100px"
// Keyword defaults
start: "top bottom" // element top at viewport bottom (default)
end: "bottom top" // element bottom at viewport top
| Edge Value | Meaning |
|---|---|
top | Top of the element or viewport |
bottom | Bottom of the element or viewport |
center | Center (50%) |
80% | 80% from the viewport top |
+=100px / -=100px | Offset in pixels |
You might be wondering: Why is the default start: "top bottom"? Because the most common trigger is “element’s top appears at the bottom of the viewport” — right when you scroll it into view.
Pinning — Locking Elements in Place
Pin an element in place while scroll passes through a range. The element stays fixed until the end is reached, then scrolls away naturally.
ScrollTrigger.create({
trigger: ".pin-section",
start: "top top", // when section top hits viewport top
end: "+=500", // 500px of scrolling later
pin: true,
pinSpacing: true // adds padding to prevent layout jumps
});How it works:
- User scrolls until
.pin-section’s top reaches the viewport top - ScrollTrigger locks the section in place (adds
position: fixed) pinSpacing: trueadds a spacer div to prevent the rest of the page from snapping up- After 500px of scrolling, the section unpins and flows away
Why pinSpacing matters: Without it, when the pinned element becomes fixed, the rest of the page collapses upward. With it, the page maintains its natural height.
Scrubbing — Animation Follows the Scrollbar
Scrub ties animation progress directly to the scrollbar. Scroll forward = animation plays forward. Scroll back = animation rewinds.
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".progress-section",
start: "top center",
end: "bottom center",
scrub: true // instant sync
// scrub: 1 // smooth sync with 1s lag
}
});
tl.to(".progress-bar", { width: "100%", duration: 1 });scrub: true vs scrub: 1:
true= instant sync. The animation jumps as you scroll.1(or any number) = smooth interpolation with that many seconds of lag. Feels more polished.
When scrub is enabled, the tween’s duration is ignored — scroll position fully controls progress.
Toggle Actions — Four-Phase Control
toggleActions controls what happens at each scroll boundary. It’s a 4-word string:
toggleActions: "play pause resume reset"| Position | Phase | Meaning |
|---|---|---|
| 1st | onEnter | Element scrolls into view |
| 2nd | onLeave | Element scrolls past view |
| 3rd | onEnterBack | Element re-enters from below |
| 4th | onLeaveBack | Element leaves upward |
Available actions: play, pause, resume, reset, restart, complete, reverse, none.
// Play once when entering, ignore everything else
toggleActions: "play none none none"
// Play forward on enter, reverse on leave
toggleActions: "play none none reverse"
// Restart every time it enters view
toggleActions: "restart none none none"Markers — Debugging Made Visible
Always enable markers: true during development. They show colored guide lines in the viewport.
ScrollTrigger.create({
trigger: ".debug-section",
start: "top 80%",
end: "bottom 20%",
markers: true,
onEnter: () => { /* your code */ }
});- Green line = start point
- Red line = end point
- Element outline = the trigger element
Important: Remove markers before deploying to production. They create visual noise and a minor performance cost.
Parallax — Creating Depth
Parallax moves elements at different speeds while scrolling, creating an illusion of depth.
gsap.to(".parallax-layer", {
y: (i, el) => -parseFloat(el.getAttribute("data-speed")) * 200,
ease: "none",
scrollTrigger: {
trigger: ".parallax-section",
start: "top bottom",
end: "bottom top",
scrub: true
}
});A cleaner approach with data attributes:
<div class="parallax-layer" data-speed="0.3"></div>
<div class="parallax-layer" data-speed="0.6"></div>gsap.utils.toArray(".parallax-layer").forEach(layer => {
const speed = parseFloat(layer.getAttribute("data-speed")) || 0.2;
gsap.to(layer, {
y: () => window.innerHeight * speed,
ease: "none",
scrollTrigger: {
trigger: layer.parentElement,
start: "top bottom",
end: "bottom top",
scrub: true
}
});
});Why parallax matters: Doda Browser’s new tab page uses subtle parallax on background patterns — it adds depth without distracting from content.
Horizontal Scrolling
Create a horizontal scroll section by translating panels sideways while pinning the container.
const sections = gsap.utils.toArray(".panel");
gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
ease: "none",
scrollTrigger: {
trigger: ".horizontal-container",
pin: true,
scrub: 1,
start: "top top",
end: () => `+=${document.querySelector(".horizontal-container").scrollWidth}`,
invalidateOnRefresh: true
}
});Each .panel should have width: 100vw and be laid out in a horizontal flex row.
Responsive with matchMedia()
Different devices need different animations. ScrollTrigger.matchMedia() handles breakpoint-specific code.
ScrollTrigger.matchMedia({
// Desktop (≥1024px)
"(min-width: 1024px)": function() {
gsap.to(".card", {
x: 200,
scrollTrigger: { trigger: ".card", scrub: true }
});
},
// Tablet (768px - 1023px)
"(min-width: 768px) and (max-width: 1023px)": function() {
gsap.to(".card", {
y: 100,
scrollTrigger: { trigger: ".card", scrub: true }
});
},
// Mobile (<768px) — no animation
"(max-width: 767px)": function() {
// Return nothing (or cleanup function)
}
});Any ScrollTrigger instances created inside are automatically reverted when the media query no longer matches.
Refreshing — Recalculating After Layout Changes
Images, dynamic content, and font loads change element positions. Refresh to recalculate.
window.addEventListener("load", () => ScrollTrigger.refresh());
// After dynamic content
function addContent() {
container.innerHTML = newHTML;
ScrollTrigger.refresh();
}Use invalidateOnRefresh: true on individual triggers that depend on element dimensions.
Common Mistakes
1. Forgetting to register ScrollTrigger
It silently fails. Always call gsap.registerPlugin(ScrollTrigger) once.
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger); // required
2. Reversed start/end syntax
The format is "{trigger-edge} {viewport-edge}". The wrong order causes weird behavior.
// Wrong
start: "80% top"
// Correct
start: "top 80%"3. Animating the trigger element itself without proper pinning
If the trigger is moving (e.g., translating Y) and also pinned, the pin can override the animation. Use a wrapper as trigger.
// Problematic: trigger is also the animated target
gsap.to(".moving-card", {
y: 100,
scrollTrigger: { trigger: ".moving-card", pin: true }
});
// Fixed: wrapper triggers, inner animates
gsap.to(".moving-card-inner", {
y: 100,
scrollTrigger: { trigger: ".moving-card-wrapper", pin: true }
});4. Not refreshing after layout changes
Images loading, dynamic content, or font swaps change positions. Without refresh, start/end points are stale.
5. Using scrub: true with non-zero duration on the tween
When scrub is on, the tween’s duration is ignored — scroll position fully controls progress. Remove duration or understand it doesn’t matter.
// duration is meaningless when scrub is true
gsap.to(".el", { x: 200, duration: 5, scrollTrigger: { scrub: true } });6. Leaving markers in production
Markers create visual noise and a tiny performance cost. Strip them before deploying.
7. Not cleaning up ScrollTrigger in SPAs
Call ScrollTrigger.getAll().forEach(st => st.kill()) when destroying a component. Otherwise, triggers keep listening.
Practice Questions
What is the difference between
scrub: trueandtoggleActions: "play reverse play reverse"?scrub: trueties animation progress 1:1 to the scrollbar — forward = forward, back = backward.toggleActionsplays the full animation when entering and reverses when leaving — not frame-accurate.Why does
pinSpacing: truematter?Without it, when an element is pinned (becomes
position: fixed), the page collapses upward because the element is removed from the document flow.pinSpacinginserts a spacer to maintain natural page height.What happens if you forget
gsap.registerPlugin(ScrollTrigger)?ScrollTrigger silently fails. No errors, no warnings, no animations. It’s the #1 debugging issue.
When would you use
ScrollTrigger.matchMedia()?When your scroll animation needs different behavior on desktop vs mobile. For example, parallax on desktop, no animation on mobile.
Why should you call
ScrollTrigger.refresh()after images load?Images change element heights and positions. Without refresh, start/end trigger points are calculated with wrong dimensions.
Challenge
Build a “product reveal” page with three sections: (1) a hero with parallax background, (2) pinned feature cards that animate in sequence as you scroll, and (3) a scrub-tied progress bar. Use matchMedia to disable the parallax on mobile.
Solution Outline
ScrollTrigger.matchMedia({
"(min-width: 768px)": function() {
// Parallax hero
gsap.to(".hero-bg", {
y: 150, ease: "none",
scrollTrigger: { trigger: ".hero", start: "top bottom", end: "bottom top", scrub: true }
});
},
"(max-width: 767px)": function() {
// No parallax on mobile
}
});
// Pinned cards with stagger
const tl = gsap.timeline({
scrollTrigger: { trigger: ".features", pin: true, start: "top top", end: "+=800", scrub: 1 }
});
tl.from(".feature-card", { y: 100, opacity: 0, stagger: 0.2 });
// Progress bar
gsap.to(".progress", {
width: "100%", ease: "none",
scrollTrigger: { trigger: "body", start: "top top", end: "bottom bottom", scrub: true }
});FAQ
Try It Yourself — ScrollStory
Build a multi-section scrolling narrative with pinned sections, fade-in cards, parallax backgrounds, a progress bar, and scrub-tied animations. This is similar to the scroll-driven onboarding flow in Doda Browser.
What’s Next
Now that you can build scroll-driven narratives, refine your motion quality and performance.
| Tutorial | What You’ll Learn |
|---|---|
| https://tutorials.dodatech.com/frontend/libraries/gsap/gsap-easing-motion/ | Natural easing and motion paths |
| https://tutorials.dodatech.com/frontend/libraries/gsap/gsap-performance/ | Keeping animations at 60fps |
| https://tutorials.dodatech.com/frontend/libraries/gsap/gsap-timelines-sequencing/ | Sequencing multiple animations |
Related topics: CSS Scroll Animations, JavaScript Events, React + GSAP
What’s Next
Congratulations on completing this Gsap Scrolltrigger tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro