plugins/bundles/core-3d-animation/skills/gsap-scrolltrigger/SKILL.md
Comprehensive skill for GSAP (GreenSock Animation Platform) and ScrollTrigger plugin. Use this skill when creating web animations, scroll-driven experiences, timelines, tweens, scroll-triggered animations, pinning, scrubbing, parallax effects, or animating DOM elements, SVG, Canvas, WebGL, or Three.js. Triggers on tasks involving GSAP, ScrollTrigger, smooth animations, scroll effects, or animation sequencing.
npx skillsauth add freshtechbro/claudedesignskills gsap-scrolltriggerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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 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"
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
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
}
});
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
// 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
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
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
}
});
ScrollTrigger.create({
trigger: ".panel",
start: "top top",
end: "+=500", // Pin for 500px of scrolling
pin: true,
pinSpacing: true // Add spacing (default true)
});
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
}
});
// 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
}
});
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");
// 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
});
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"
}
});
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
}
});
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>
);
}
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" />;
}
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();
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
});
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 }
});
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
}
});
}
});
will-change CSS.animated-element {
will-change: transform, opacity;
}
// 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
// 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();
};
}, []);
ScrollTrigger handles this automatically, but for custom resize logic:
let resizeTimer;
window.addEventListener("resize", () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
ScrollTrigger.refresh();
}, 250);
});
invalidateOnRefreshFor 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
}
});
// 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 });
// 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 }
});
});
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin); // Must register!
// 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 });
// 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"
// 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
});
This skill includes bundled resources:
api_reference.md: Quick API reference (tween methods, timeline methods, ScrollTrigger properties)easing_guide.md: Visual easing reference with use casescommon_patterns.md: Copy-paste patterns for common scenariosgenerate_animation.py: Generate boilerplate GSAP codetimeline_builder.py: Interactive timeline sequence builderstarter_scroll/: Complete scroll-driven site templateeasings/: Easing visualization HTML toolexamples/: Real-world ScrollTrigger examplesUse this skill when:
For Three.js-specific animations, also reference the threejs-webgl skill. For React components with built-in animations, reference the motion-framer skill.
development
Meta-skill for combining Three.js, GSAP ScrollTrigger, React Three Fiber, Motion, and React Spring for complex 3D web experiences. Use when building applications that integrate multiple 3D and animation libraries, requiring architecture patterns, state management, and performance optimization across the stack. Triggers on tasks involving library integration, multi-library architectures, scroll-driven 3D experiences, physics-based 3D animations, or complex interactive 3D applications.
development
Comprehensive skill for Three.js 3D web development. Use this skill when building interactive 3D scenes, WebGL/WebGPU applications, product configurators, 3D visualizations, or immersive web experiences. Triggers on tasks involving Three.js, 3D rendering, scenes, cameras, meshes, materials, lights, animations, textures, or WebGL/WebGPU rendering.
tools
Comprehensive skill for Adobe Substance 3D Painter texturing and material creation workflow. Use this skill when creating PBR materials, exporting textures for web/game engines, optimizing 3D assets for real-time rendering, or automating texture workflows. Triggers on tasks involving Substance 3D Painter, PBR texturing, material creation, texture export for Three.js, Babylon.js, Unity, Unreal, glTF optimization, or Python API automation. Creates optimized textures for threejs-webgl, react-three-fiber, and babylonjs-engine materials.
tools
Browser-based 3D design tool with visual editor, animation, and web export. Use this skill when creating 3D scenes without code, designing interactive web experiences, prototyping 3D UI, exporting to React/web, or building designer-friendly 3D content. Triggers on tasks involving Spline, no-code 3D, visual 3D editor, 3D animation, state-based interactions, React Spline integration, or scene export. Alternative to Three.js for designers who prefer visual tools over code.