GSAP Cheatsheet
ScrollTrigger
Use this GSAP reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Setup
import gsap from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; gsap.registerPlugin(ScrollTrigger);
Minimal Usage
// Trigger a tween when .section enters the viewport gsap.to(".box", { x: 200, scrollTrigger: ".section", // shorthand: trigger element selector }); // Full inline config gsap.to(".box", { x: 200, scrollTrigger: { trigger: ".section", start: "top 80%", // when top of .section hits 80% from top of viewport end: "bottom 20%", // when bottom of .section hits 20% from top of viewport toggleActions: "play none none reverse", }, });
ScrollTrigger.create()
Standalone ScrollTrigger — not attached to a tween.
const st = ScrollTrigger.create({ trigger: ".panel", start: "top top", end: "+=500", pin: true, onEnter: ({ progress, direction, isActive }) => console.log("enter"), onLeave: () => console.log("leave"), onEnterBack: () => console.log("enter back"), onLeaveBack: () => console.log("leave back"), onUpdate: (self) => console.log(self.progress), onToggle: (self) => console.log(self.isActive), markers: true, // visual debug markers (remove in production!) }); st.kill(); // destroy st.refresh(); // recalculate positions
Core Config Properties
| Property | Type | Description |
|---|---|---|
trigger | selector / element | Element whose position controls the trigger |
start | string / number / function | Trigger start (see syntax below) |
end | string / number / function | Trigger end |
scroller | selector / element | Custom scroll container (default: window) |
scrub | boolean / number | Link animation progress to scroll (true or seconds to smooth) |
pin | boolean / selector | Pin the trigger element for the scroll duration |
pinSpacing | boolean / string | Add space after pinned element (default: true) |
pinReparent | boolean | Move pinned element to <body> (for overflow:hidden parents) |
toggleClass | string / object | Add/remove CSS class when active |
toggleActions | string | 4-part string (onEnter, onLeave, onEnterBack, onLeaveBack) |
once | boolean | Kill after first activation |
markers | boolean / object | Debug markers |
horizontal | boolean | Horizontal scroll mode |
invalidateOnRefresh | boolean | Invalidate animation values on resize/refresh |
anticipatePin | number | Seconds to look ahead for pin (prevents jank on mobile) |
fastScrollEnd | boolean / number | Snap playhead to end if scrolling fast |
start / end Syntax
Format: "triggerPoint viewportPoint"
start: "top top" // trigger top hits viewport top start: "top 80%" // trigger top hits 80% from viewport top start: "top center" // trigger top hits viewport center start: "center center" start: "bottom bottom" start: "top top+=100" // +100px offset start: "top -=50" // -50px offset start: "50% 50%" // both at 50% // Pixel offset from the start end: "+=500" // 500px after start end: "+=100%" // 100% of viewport height after start // Numeric (absolute scroll position in px) start: 400 end: 900 // Function (called on refresh) start: () => window.innerHeight * 0.5
Viewport reference words: top, center, bottom, left, right (or pixel/percent values)
toggleActions
Four action keywords for the four scroll states, space-separated:
"onEnter onLeave onEnterBack onLeaveBack"
| Keyword | Meaning |
|---|---|
"play" | Play forward |
"pause" | Pause |
"resume" | Resume from current position |
"reset" | Jump to time 0 (doesn't play) |
"restart" | Reset and play from beginning |
"complete" | Jump to end |
"reverse" | Play backward |
"none" | Do nothing |
toggleActions: "play none none reverse" // (default-ish) play on enter, reverse on scroll back toggleActions: "play pause resume none" toggleActions: "restart none none none" // restart every time you enter toggleActions: "play complete reverse reset"
scrub
Links animation progress directly to the scroll position.
// Boolean — instant scrub (no smoothing) gsap.to(".parallax", { y: -200, scrollTrigger: { trigger: ".section", scrub: true }, }); // Number — smoothing lag in seconds gsap.to(".parallax", { y: -200, scrollTrigger: { trigger: ".section", scrub: 1 }, // 1s to catch up }); // With a timeline const tl = gsap.timeline({ scrollTrigger: { trigger: ".scroll-section", start: "top top", end: "+=3000", scrub: 1, pin: true, }, }); tl.from(".a", { x: -200 }) .to(".b", { y: 100 }) .to(".c", { scale: 0.5 }, "<");
pin
Freezes the trigger element in place while the scroll progresses.
ScrollTrigger.create({ trigger: ".hero", start: "top top", end: "+=800", pin: true, // pin .hero for 800px of scroll pinSpacing: true, // add space below so next content is pushed down }); // Pin a parent, animate children const tl = gsap.timeline({ scrollTrigger: { trigger: ".slides-container", pin: true, scrub: 1, end: "+=3000", }, }); tl.to(".slide-1", { xPercent: -100 }) .to(".slide-2", { xPercent: -100 });
Callbacks in ScrollTrigger
ScrollTrigger.create({ trigger: ".section", onEnter: (self) => console.log("entered, progress:", self.progress), onLeave: (self) => console.log("left"), onEnterBack: (self) => console.log("entered back"), onLeaveBack: (self) => console.log("left back"), onUpdate: (self) => { // fires every scroll tick while active console.log(self.progress, self.direction, self.velocity); }, onToggle: (self) => console.log("active:", self.isActive), onRefresh: (self) => console.log("positions recalculated"), onScrubComplete: (self) => console.log("scrub settled"), });
toggleClass
ScrollTrigger.create({ trigger: ".nav", start: "top top", toggleClass: { targets: ".nav", className: "is-sticky" }, }); // Multiple targets toggleClass: { targets: [".nav", ".logo"], className: "scrolled" }
Horizontal Scroll
// Horizontal page scroll const sections = gsap.utils.toArray(".panel"); gsap.to(sections, { xPercent: -100 * (sections.length - 1), ease: "none", scrollTrigger: { trigger: ".horizontal-container", pin: true, scrub: 1, snap: 1 / (sections.length - 1), end: () => "+=" + document.querySelector(".horizontal-container").offsetWidth, }, });
snap
scrollTrigger: {
snap: 0.25, // snap to every 25% of progress
snap: [0, 0.33, 0.66, 1], // snap to specific progress values
snap: {
snapTo: "labels", // snap to timeline labels
duration: { min: 0.2, max: 0.5 }, // snap duration range
delay: 0.1,
ease: "power1.inOut",
},
}Refresh & Resize
// Force a full position recalculation ScrollTrigger.refresh(); // Recalculate after layout changes window.addEventListener("load", () => ScrollTrigger.refresh()); // Refresh only a specific trigger const st = ScrollTrigger.create({ trigger: ".box" }); st.refresh(); // Disable / re-enable all ScrollTrigger.disable(); ScrollTrigger.enable();
ScrollTrigger.batch()
Animate groups of elements as they enter the viewport — efficient alternative to one trigger per element.
ScrollTrigger.batch(".card", { onEnter: (elements) => { gsap.from(elements, { opacity: 0, y: 40, stagger: 0.1, duration: 0.6, ease: "power2.out", }); }, onLeave: (els) => gsap.set(els, { opacity: 0 }), onEnterBack: (els) => gsap.to(els, { opacity: 1 }), start: "top 85%", once: true, });
Scrolling a Container (not window)
ScrollTrigger.create({ trigger: ".item", scroller: ".scroll-container", // CSS overflow: scroll element start: "top 80%", }); // Or set globally ScrollTrigger.defaults({ scroller: ".app-scroll" });
getAll() and kill helpers
// Array of all ScrollTrigger instances ScrollTrigger.getAll().forEach(st => st.kill()); // Kill a specific instance by ID const st = ScrollTrigger.create({ id: "hero", trigger: ".hero" }); ScrollTrigger.getById("hero").kill(); // Kill and revert (un-pin, restore styles) ScrollTrigger.getAll().forEach(st => st.kill(true));
Responsive: gsap.matchMedia()
gsap.matchMedia() (3.11+) is the standard way to make triggers responsive — each setup re-runs when its media query starts matching, and everything created inside is reverted automatically when it stops.
const mm = gsap.matchMedia(); mm.add("(min-width: 1024px)", () => { gsap.to(".box", { x: 300, scrollTrigger: { trigger: ".section", start: "top center", scrub: true }, }); return () => console.log("extra cleanup on breakpoint change"); // optional }); mm.add("(max-width: 1023px)", () => { gsap.to(".box", { y: 100, scrollTrigger: ".section" }); }); mm.revert(); // kill + revert everything this matchMedia created
// Named conditions — one handler for several queries mm.add( { isDesktop: "(min-width: 1024px)", isMobile: "(max-width: 1023px)", reduceMotion: "(prefers-reduced-motion: reduce)", }, (context) => { const { isDesktop, reduceMotion } = context.conditions; gsap.to(".hero", { x: isDesktop && !reduceMotion ? 300 : 0, scrollTrigger: ".section", }); } );
Legacy:
ScrollTrigger.matchMedia({ "(min-width: …)": fn })(pre-3.11) still runs but is deprecated — migrate togsap.matchMedia().
ScrollToPlugin, ScrollSmoother & Observer
All free (ScrollSmoother since GSAP 3.13).
// ScrollToPlugin — tween the scroll position itself import { ScrollToPlugin } from "gsap/ScrollToPlugin"; gsap.registerPlugin(ScrollToPlugin); gsap.to(window, { scrollTo: "#section3", duration: 1, ease: "power2.inOut" }); gsap.to(window, { scrollTo: { y: "#section3", offsetY: 80 } }); // stop 80px above
// ScrollSmoother — smooth-scrolls the whole page (built on ScrollTrigger) import { ScrollSmoother } from "gsap/ScrollSmoother"; gsap.registerPlugin(ScrollTrigger, ScrollSmoother); ScrollSmoother.create({ wrapper: "#smooth-wrapper", // outer element content: "#smooth-content", // direct child holding the page smooth: 1.2, // seconds to "catch up" to native scroll effects: true, // enable data-speed / data-lag parallax attributes });
// Observer — unified wheel / touch / pointer deltas (no scrollbar needed) import { Observer } from "gsap/Observer"; gsap.registerPlugin(Observer); Observer.create({ target: window, type: "wheel,touch,pointer", onUp: () => gotoSection(index - 1), onDown: () => gotoSection(index + 1), tolerance: 10, preventDefault: true, });
Common Gotchas
- Always run
ScrollTrigger.refresh()after images load or fonts shift layout. scrubandtoggleActionsare mutually exclusive — with scrub, toggleActions is ignored.pin: trueaddspadding-bottomto compensate for the pinned element's duration — this can break flex/grid layouts. UsepinSpacing: falsecarefully.- Inside React, use the
useGSAP()hook from@gsap/react(orgsap.context()+ctx.revert()on unmount) so ScrollTriggers are killed with the component. - On mobile, browser chrome (address bar) changes viewport height — use
document.documentElement.clientHeightinstead ofwindow.innerHeightfor start/end functions.