GSAP & ScrollTrigger Development
Overview
GSAP (GreenSock Animation Platform) is the industry-leading JavaScript animation library for creating high-performance, production-quality animations. ScrollTrigger is GSAP's powerful plugin for scroll-driven animations. Together, they enable everything from simple UI transitions to complex scroll-based storytelling experiences.
Core Concepts
The Basics: Tweens
A tween is a single animation from point A to point B.
// Animate TO a state (from current) gsap.to(".box", { x: 200, rotation: 360, duration: 1, ease: "power2.inOut" });
// Animate FROM a state (to current) gsap.from(".box", { opacity: 0, y: -50, duration: 0.8 });
// Animate FROM-TO (define both start and end) gsap.fromTo(".box", { opacity: 0, scale: 0.5 }, // FROM { opacity: 1, scale: 1, duration: 1 } // TO );
Timelines: Sequencing Animations
Timelines orchestrate multiple tweens in sequence or overlap.
const tl = gsap.timeline();
// Sequential by default tl.to(".box1", { x: 100, duration: 1 }) .to(".box2", { y: 100, duration: 1 }) .to(".box3", { rotation: 360, duration: 1 });
// With labels for organization tl.addLabel("start") .to(".hero", { opacity: 1, duration: 1 }) .addLabel("reveal") .to(".content", { y: 0, duration: 0.8 }, "reveal") // Start at "reveal" label .to(".cta", { scale: 1, duration: 0.5 }, "reveal+=0.5"); // 0.5s after "reveal"
Position Parameter (Timeline Timing)
Control when animations start within a timeline:
const tl = gsap.timeline();
// Default: One after another tl.to(".box1", { x: 100 }) .to(".box2", { x: 100 }); // Starts after box1 finishes
// Start at the same time tl.to(".box1", { x: 100 }) .to(".box2", { y: 100 }, 0); // Starts at 0 seconds
// Relative positioning tl.to(".box1", { x: 100, duration: 2 }) .to(".box2", { y: 100 }, "-=1"); // Starts 1 second before box1 ends .to(".box3", { rotation: 360 }, "+=0.5"); // Starts 0.5s after box2 finishes
// At a specific time tl.to(".box1", { x: 100 }, 2.5); // Starts at 2.5 seconds
ScrollTrigger Fundamentals
Basic Scroll Animation
gsap.registerPlugin(ScrollTrigger);
gsap.to(".box", { x: 500, scrollTrigger: { trigger: ".box", start: "top center", // When top of trigger hits center of viewport end: "bottom center", markers: true, // Development only - shows start/end positions scrub: true, // Links animation to scrollbar toggleActions: "play none none reverse" // onEnter onLeave onEnterBack onLeaveBack } });
Start & End Positions
Format: "[trigger position] [viewport position]"
// Common patterns start: "top top" // Trigger top hits viewport top start: "top center" // Trigger top hits viewport center (default) start: "top bottom" // Trigger top hits viewport bottom start: "center center" // Trigger center hits viewport center
// With offsets start: "top top+=100" // 100px below viewport top start: "top 80%" // 80% down the viewport end: "+=500" // 500px after start position end: "bottom top" // Trigger bottom hits viewport top
Scrubbing (Scroll-Synced Animation)
// Boolean: Direct link to scrollbar (immediate) scrub: true
// Number: Smoothing delay in seconds scrub: 1 // Takes 1 second to "catch up" to scrollbar scrub: 0.5 // Faster, tighter feel
Toggle Actions
Control animation at four scroll points:
toggleActions: "play pause resume reset" // onEnter | onLeave | onEnterBack | onLeaveBack
// Actions: play, pause, resume, restart, reset, complete, reverse, none
Common patterns:
toggleActions: "play none none none" // Play once on enter toggleActions: "play none none reverse" // Play forward, reverse back toggleActions: "play complete reverse reset" // Full control toggleActions: "restart pause resume pause" // Restart on each enter
Common Patterns
- Fade In On Scroll
gsap.from(".fade-in", { opacity: 0, y: 50, duration: 1, scrollTrigger: { trigger: ".fade-in", start: "top 80%", end: "top 50%", scrub: 1, once: true // Only animate once } });
- Pin Element While Scrolling
ScrollTrigger.create({ trigger: ".panel", start: "top top", end: "+=500", // Pin for 500px of scrolling pin: true, pinSpacing: true // Add spacing (default true) });
- Horizontal Scroll Section
const sections = gsap.utils.toArray(".panel");
gsap.to(sections, { xPercent: -100 * (sections.length - 1), ease: "none", scrollTrigger: { trigger: ".container", pin: true, scrub: 1, end: () => "+=" + document.querySelector(".container").offsetWidth } });
- Parallax Effect
// Slower movement (background layer) gsap.to(".bg", { y: 200, ease: "none", scrollTrigger: { trigger: ".section", start: "top bottom", end: "bottom top", scrub: true } });
// Faster movement (foreground layer) gsap.to(".fg", { y: -100, ease: "none", scrollTrigger: { trigger: ".section", start: "top bottom", end: "bottom top", scrub: true } });
- Scroll-Triggered Timeline
const tl = gsap.timeline({ scrollTrigger: { trigger: ".container", start: "top top", end: "+=500", scrub: 1, pin: true, snap: { snapTo: "labels", // Snap to timeline labels duration: { min: 0.2, max: 3 }, delay: 0.2, ease: "power1.inOut" } } });
tl.addLabel("start") .from(".title", { scale: 0.3, rotation: 45, autoAlpha: 0 }) .addLabel("color") .from(".box", { backgroundColor: "#28a92b" }) .addLabel("spin") .to(".box", { rotation: 360 }) .addLabel("end");
- Batch Animations (Multiple Elements)
// Loop through multiple elements gsap.utils.toArray(".box").forEach((box, i) => { gsap.from(box, { y: 100, opacity: 0, scrollTrigger: { trigger: box, start: "top 80%", end: "top 50%", scrub: 1 } }); });
// Or use ScrollTrigger.batch ScrollTrigger.batch(".box", { onEnter: batch => gsap.to(batch, { opacity: 1, y: 0, stagger: 0.15 }), onLeave: batch => gsap.set(batch, { opacity: 0 }), start: "top 80%", once: true });
- Staggered Animations
gsap.from(".item", { y: 50, opacity: 0, duration: 0.8, stagger: 0.1, // 0.1s between each item scrollTrigger: { trigger: ".grid", start: "top 80%" } });
// Advanced stagger gsap.from(".item", { scale: 0, duration: 1, stagger: { each: 0.1, from: "center", // "start", "center", "end", "edges", or index number grid: "auto", // For grid layouts ease: "power2.inOut" } });
Integration Patterns
With Three.js / WebGL
import * as THREE from 'three'; import gsap from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
// Animate camera gsap.to(camera.position, { x: 5, y: 3, z: 10, scrollTrigger: { trigger: "#section2", start: "top top", end: "bottom top", scrub: 1, onUpdate: () => camera.lookAt(scene.position) } });
// Animate mesh rotation gsap.to(mesh.rotation, { y: Math.PI * 2, scrollTrigger: { trigger: "#section3", start: "top bottom", end: "bottom top", scrub: true } });
// Animate material properties gsap.to(material, { opacity: 0, scrollTrigger: { trigger: "#section4", start: "top center", end: "center center", scrub: 1 } });
With React (useGSAP Hook)
import { useRef } from 'react'; import { useGSAP } from '@gsap/react'; import gsap from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
function Component() { const container = useRef(); const box = useRef();
useGSAP(() => { gsap.to(box.current, { x: 200, scrollTrigger: { trigger: box.current, start: "top center", end: "bottom center", scrub: true, markers: true } }); }, { scope: container }); // Scoping for cleanup
return ( <div ref={container}> <div ref={box} className="box">Animated Box</div> </div> ); }
Sharing Timeline in React
function App() { const [tl, setTl] = useState();
useGSAP(() => { const timeline = gsap.timeline(); setTl(timeline); }, []);
return ( <div> <Box timeline={tl} index={0} /> <Circle timeline={tl} index={1} /> </div> ); }
function Box({ timeline, index }) { const ref = useRef();
useGSAP(() => { timeline && timeline.to(ref.current, { x: 100 }, index * 0.1); }, [timeline, index]);
return <div ref={ref} className="box" />; }
Locomotive Scroll Integration
import LocomotiveScroll from 'locomotive-scroll';
const scroller = new LocomotiveScroll({ el: document.querySelector('[data-scroll-container]'), smooth: true });
ScrollTrigger.scrollerProxy("[data-scroll-container]", { scrollTop(value) { return arguments.length ? scroller.scrollTo(value, 0, 0) : scroller.scroll.instance.scroll.y; }, getBoundingClientRect() { return {top: 0, left: 0, width: window.innerWidth, height: window.innerHeight}; }, pinType: document.querySelector("[data-scroll-container]").style.transform ? "transform" : "fixed" });
ScrollTrigger.addEventListener("refresh", () => scroller.update()); ScrollTrigger.refresh();
Advanced Techniques
Image Sequence Scrubbing
const canvas = document.querySelector("canvas"); const context = canvas.getContext("2d");
const images = []; const imageCount = 147; const currentFrame = { value: 0 };
for (let i = 0; i < imageCount; i++) {
const img = new Image();
img.src = ./frames/frame_${i.toString().padStart(4, '0')}.jpg;
images.push(img);
}
images[0].onload = () => { canvas.width = images[0].width; canvas.height = images[0].height; render(); };
function render() { context.clearRect(0, 0, canvas.width, canvas.height); context.drawImage(images[Math.floor(currentFrame.value)], 0, 0); }
gsap.to(currentFrame, { value: imageCount - 1, snap: "value", ease: "none", scrollTrigger: { trigger: canvas, start: "top top", end: "+=500%", scrub: true, pin: true }, onUpdate: render });
Smooth Scroll to Element
gsap.registerPlugin(ScrollToPlugin);
// Scroll to element gsap.to(window, { duration: 1, scrollTo: "#section2", ease: "power2.inOut" });
// With offset gsap.to(window, { duration: 1.5, scrollTo: { y: "#section2", offsetY: 50 }, ease: "expo.inOut" });
// Horizontal scroll gsap.to(".container", { duration: 2, scrollTo: { x: 1000, autoKill: true } });
Conditional Animations (Media Queries)
ScrollTrigger.matchMedia({ // Desktop "(min-width: 800px)": function() { gsap.to(".box", { x: 500, scrollTrigger: { trigger: ".box", start: "top center", end: "bottom top", scrub: true } }); },
// Mobile "(max-width: 799px)": function() { gsap.to(".box", { y: 200, scrollTrigger: { trigger: ".box", start: "top 80%", scrub: 1 } }); } });
Performance Best Practices
- Use will-change CSS
.animated-element { will-change: transform, opacity; }
- Limit Repaints
// Good: Animate transform/opacity (GPU accelerated) gsap.to(".box", { x: 100, opacity: 0.5 });
// Avoid: Animating layout properties // gsap.to(".box", { width: 500, height: 300 }); // Causes reflow
- Dispose of ScrollTriggers
// Kill individual trigger const trigger = ScrollTrigger.create({ /* ... */ }); trigger.kill();
// Kill all triggers ScrollTrigger.getAll().forEach(t => t.kill());
// In React with cleanup useGSAP(() => { const tween = gsap.to(".box", { /* ... */ });
return () => { tween.kill(); }; }, []);
- Debounce Resize
ScrollTrigger handles this automatically, but for custom resize logic:
let resizeTimer; window.addEventListener("resize", () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { ScrollTrigger.refresh(); }, 250); });
- Use invalidateOnRefresh
For dynamic values that change on resize:
gsap.to(".box", { x: () => window.innerWidth / 2, // Dynamic value scrollTrigger: { trigger: ".box", start: "top center", invalidateOnRefresh: true // Recalculate x on resize } });
Common Pitfalls
- Multiple Tweens on Same Element
// Problem: Second tween conflicts with first gsap.to('h1', { x: 100, scrollTrigger: { /* ... / } }); gsap.to('h1', { x: 200, scrollTrigger: { / ... */ } }); // Jumps!
// Solution 1: Use fromTo gsap.fromTo('h1', { x: 100 }, { x: 200, scrollTrigger: { /* ... */ } });
// Solution 2: Use immediateRender: false gsap.to('h1', { x: 200, immediateRender: false, scrollTrigger: { /* ... */ } });
// Solution 3: Apply ScrollTrigger to timeline const tl = gsap.timeline({ scrollTrigger: { /* ... */ } }); tl.to('h1', { x: 100 }) .to('h1', { x: 200 });
- Not Using Loops for Multiple Elements
// Wrong: Animates all at once gsap.to('.section', { y: -100, scrollTrigger: { trigger: '.section', scrub: true } });
// Right: Loop for individual triggers gsap.utils.toArray('.section').forEach(section => { gsap.to(section, { y: -100, scrollTrigger: { trigger: section, scrub: true } }); });
- Forgetting to Register Plugins
import gsap from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin); // Must register!
- Nested ScrollTriggers in Timelines
// Wrong: ScrollTriggers on individual tweens in timeline const tl = gsap.timeline(); tl.to('.box1', { x: 100, scrollTrigger: { /* ... / } }) // Don't do this! .to('.box2', { y: 100, scrollTrigger: { / ... */ } });
// Right: ScrollTrigger on parent timeline const tl = gsap.timeline({ scrollTrigger: { /* ... */ } }); tl.to('.box1', { x: 100 }) .to('.box2', { y: 100 });
Easing Reference
// Power easings (most common) ease: "power1.out" // Subtle deceleration ease: "power2.inOut" // Smooth acceleration/deceleration ease: "power3.in" // Strong acceleration ease: "power4.out" // Very strong deceleration
// Special easings ease: "elastic.out" // Bouncy overshoot ease: "back.out" // Slight overshoot ease: "bounce.out" // Bouncing effect ease: "circ.inOut" // Circular motion feel ease: "expo.inOut" // Exponential (dramatic)
// Linear (for scrubbed scroll animations) ease: "none"
ScrollTrigger Methods
// Refresh all ScrollTriggers (after DOM changes) ScrollTrigger.refresh();
// Get all ScrollTriggers const triggers = ScrollTrigger.getAll();
// Get specific trigger by ID const st = ScrollTrigger.getById("myTrigger");
// Kill trigger st.kill();
// Update trigger st.scroll(500); // Programmatically set scroll position st.enable(); st.disable();
// Global ScrollTrigger config ScrollTrigger.config({ limitCallbacks: true, // Improve performance syncInterval: 15 // Throttle scroll checks (ms) });
// Debug mode ScrollTrigger.defaults({ markers: true // Show markers on all triggers });
Resources
This skill includes bundled resources:
references/
-
api_reference.md : Quick API reference (tween methods, timeline methods, ScrollTrigger properties)
-
easing_guide.md : Visual easing reference with use cases
-
common_patterns.md : Copy-paste patterns for common scenarios
scripts/
-
generate_animation.py : Generate boilerplate GSAP code
-
timeline_builder.py : Interactive timeline sequence builder
assets/
-
starter_scroll/ : Complete scroll-driven site template
-
easings/ : Easing visualization HTML tool
-
examples/ : Real-world ScrollTrigger examples
When to Use This Skill
Use this skill when:
-
Creating smooth web animations
-
Building scroll-driven experiences
-
Implementing parallax effects
-
Sequencing complex animations
-
Animating DOM, SVG, Canvas, or WebGL
-
Integrating animations with Three.js or React
-
Building scrollytelling websites
-
Creating interactive UI transitions
For Three.js-specific animations, also reference the threejs-webgl skill. For React components with built-in animations, reference the motion-framer skill.