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

PropertyTypeDescription
triggerselector / elementElement whose position controls the trigger
startstring / number / functionTrigger start (see syntax below)
endstring / number / functionTrigger end
scrollerselector / elementCustom scroll container (default: window)
scrubboolean / numberLink animation progress to scroll (true or seconds to smooth)
pinboolean / selectorPin the trigger element for the scroll duration
pinSpacingboolean / stringAdd space after pinned element (default: true)
pinReparentbooleanMove pinned element to <body> (for overflow:hidden parents)
toggleClassstring / objectAdd/remove CSS class when active
toggleActionsstring4-part string (onEnter, onLeave, onEnterBack, onLeaveBack)
oncebooleanKill after first activation
markersboolean / objectDebug markers
horizontalbooleanHorizontal scroll mode
invalidateOnRefreshbooleanInvalidate animation values on resize/refresh
anticipatePinnumberSeconds to look ahead for pin (prevents jank on mobile)
fastScrollEndboolean / numberSnap 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"

KeywordMeaning
"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 to gsap.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.
  • scrub and toggleActions are mutually exclusive — with scrub, toggleActions is ignored.
  • pin: true adds padding-bottom to compensate for the pinned element's duration — this can break flex/grid layouts. Use pinSpacing: false carefully.
  • Inside React, use the useGSAP() hook from @gsap/react (or gsap.context() + ctx.revert() on unmount) so ScrollTriggers are killed with the component.
  • On mobile, browser chrome (address bar) changes viewport height — use document.documentElement.clientHeight instead of window.innerHeight for start/end functions.