
Gsap Framer Scroll Animation
- 2.6k installs
- 37.1k repo stars
- Updated July 28, 2026
- github/awesome-copilot
gsap-framer-scroll-animation is an agent skill that >-.
About
GSAP Framer Motion Scroll Animations Skill Production grade scroll animations with GitHub Copilot prompts ready to use code recipes and deep API references Design Companion This skill provides the technical implementation for scroll driven motion For the creative philosophy design principles and premium aesthetics that should guide how and when to animate always cross reference the premium frontend ui skill Together they form a complete approach premium frontend ui decides the what and why this skill delivers the how Need Use Vanilla JS Webflow Vue GSAP Pinning horizontal scroll complex timelines GSAP React Next js declarative style Framer Motion whileInView entrance animations Framer Motion Both in same Next js app See notes in references Read the relevant reference file for full recipes and Copilot prompts The gsap framer scroll animation agent skill provides documented workflows prerequisites triggers and safety guidance from its SKILL md source Agents load it when user requests match the description and follow step by step instructions without inventing capabilities It integrates with standard agent tooling for
- Use this skill whenever the user wants to build scroll animations, scroll effects,
- parallax, scroll-triggered reveals, pinned sections, horizontal scroll, text animations,
- or any motion tied to scroll position — in vanilla JS, React, or Next.js.
- Follow gsap-framer-scroll-animation SKILL.md steps and documented constraints.
- Follow gsap-framer-scroll-animation SKILL.md steps and documented constraints.
Gsap Framer Scroll Animation by the numbers
- 2,575 all-time installs (skills.sh)
- +221 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #299 of 16,659 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
gsap-framer-scroll-animation capabilities & compatibility
- Capabilities
- use this skill whenever the user wants to build · parallax, scroll triggered reveals, pinned secti · or any motion tied to scroll position — in vanil · follow gsap framer scroll animation skill.md ste
- Use cases
- orchestration
What gsap-framer-scroll-animation says it does
Use this skill whenever the user wants to build scroll animations, scroll effects,
parallax, scroll-triggered reveals, pinned sections, horizontal scroll, text animations,
or any motion tied to scroll position — in vanilla JS, React, or Next.js.
npx skills add https://github.com/github/awesome-copilot --skill gsap-framer-scroll-animationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.6k |
|---|---|
| repo stars | ★ 37.1k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | github/awesome-copilot ↗ |
When should an agent use gsap-framer-scroll-animation and what problem does it solve?
>-
Who is it for?
Developers invoking gsap-framer-scroll-animation as documented in the skill source.
Skip if: Skip when requirements fall outside gsap-framer-scroll-animation documented scope.
When should I use this skill?
>-
What you get
Outputs aligned with the gsap-framer-scroll-animation SKILL.md workflow and stated deliverables.
- Scroll animation components
- Parallax layers
By the numbers
- Targets Motion v12 with motion/react import path
Files
GSAP & Framer Motion — Scroll Animations Skill
Production-grade scroll animations with GitHub Copilot prompts, ready-to-use code recipes, and deep API references.
Design Companion: This skill provides the technical implementation for scroll-driven motion.
For the creative philosophy, design principles, and premium aesthetics that should guide how
and when to animate, always cross-reference the premium-frontend-ui skill.
Together they form a complete approach: premium-frontend-ui decides the what and why;
this skill delivers the how.
Quick Library Selector
| Need | Use |
|---|---|
| Vanilla JS, Webflow, Vue | GSAP |
| Pinning, horizontal scroll, complex timelines | GSAP |
| React / Next.js, declarative style | Framer Motion |
| whileInView entrance animations | Framer Motion |
| Both in same Next.js app | See notes in references |
Read the relevant reference file for full recipes and Copilot prompts:
- GSAP →
references/gsap.md— ScrollTrigger API, all recipes, React integration - Framer Motion →
references/framer.md— useScroll, useTransform, all recipes
Setup (Always Do First)
GSAP
npm install gsapimport gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger); // MUST call before any ScrollTrigger usageFramer Motion (Motion v12, 2025)
npm install motion # new package name since mid-2025
# or: npm install framer-motion — still works, same APIimport { motion, useScroll, useTransform, useSpring } from 'motion/react';
// legacy: import { motion } from 'framer-motion' — also validWorkflow
1. Interpret the user's intent to identify if GSAP or Framer Motion is the best fit. 2. Read the relevant reference document in references/ for detailed APIs and patterns. 3. Suggest the required package installation if not already present. 4. Implement the scaffold for the animation structure, adhering to the requested format (React components, hook requirements, or vanilla JS). 5. Apply the correct tools (scrolling vs in-view elements) ensuring accessibility options are present and hooks don't cause infinite re-renders.
The 5 Most Common Scroll Patterns
Quick reference — full recipes with Copilot prompts are in the reference files.
1. Fade-in on enter (GSAP)
gsap.from('.card', {
opacity: 0, y: 50, stagger: 0.15, duration: 0.8,
scrollTrigger: { trigger: '.card', start: 'top 85%' }
});2. Fade-in on enter (Framer Motion)
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.6 }}
/>3. Scrub / scroll-linked (GSAP)
gsap.to('.hero-img', {
scale: 1.3, opacity: 0, ease: 'none',
scrollTrigger: { trigger: '.hero', start: 'top top', end: 'bottom top', scrub: true }
});4. Scroll-linked (Framer Motion)
const { scrollYProgress } = useScroll({ target: ref, offset: ['start end', 'end start'] });
const y = useTransform(scrollYProgress, [0, 1], [0, -100]);
return <motion.div style={{ y }} />;5. Pinned timeline (GSAP)
const tl = gsap.timeline({
scrollTrigger: { trigger: '.section', pin: true, scrub: 1, start: 'top top', end: '+=200%' }
});
tl.from('.title', { opacity: 0, y: 60 }).from('.img', { scale: 0.85 });Critical Rules (Apply Always)
- GSAP: always call
gsap.registerPlugin(ScrollTrigger)before using it - GSAP scrub: always use
ease: 'none'— easing feels wrong when scrub is active - GSAP React: use
useGSAPfrom@gsap/react, never plainuseEffect— it auto-cleans ScrollTriggers - GSAP debug: add
markers: trueduring development; remove before production - Framer:
useTransformoutput must go intostyleprop of amotion.*element, not a plain div - Framer Next.js: always add
'use client'at top of any file using motion hooks - Both: animate only
transformandopacity— avoidwidth,height,box-shadow - Accessibility: always check
prefers-reduced-motion— see each reference file for patterns - Premium polish: follow the premium-frontend-ui skill principles for motion timing, easing curves, and restraint — animation should enhance, never overwhelm
Copilot Prompting Tips
- Give Copilot the full selector, base image, and scroll range upfront — vague prompts produce vague code
- For GSAP, always specify: selector, start/end strings, whether you want scrub or toggleActions
- For Framer, always specify: which hook (useScroll vs whileInView), offset values, what to transform
- Paste the exact error message when asking
/fix— Copilot fixes are dramatically better with real errors - Use
@workspacescope in Copilot Chat so it reads your existing component structure
Reference Files
| File | Contents |
|---|---|
references/gsap.md | Full ScrollTrigger API reference, 10 recipes, React (useGSAP), Lenis, matchMedia, accessibility |
references/framer.md | Full useScroll / useTransform API, 8 recipes, variants, Motion v12 notes, Next.js tips |
Related Skills
| Skill | Relationship |
|---|---|
| premium-frontend-ui | Creative philosophy, design principles, and aesthetic guidelines — defines when and why to animate |
Framer Motion (Motion v12) — Full Reference
Framer Motion was renamed to Motion in mid-2025. The npm package is now motion,the import path ismotion/react. All APIs are identical.framer-motionstill works.
Table of Contents
1. Package & Import Paths 2. Two Types of Scroll Animation 3. useScroll — Options Reference 4. useTransform — Full Reference 5. useSpring for Smoothing 6. Recipes with Copilot Prompts
- Scroll progress bar
- Reusable ScrollReveal wrapper
- Parallax layers
- Horizontal scroll section
- Image reveal with clipPath
- Scroll-linked navbar (hide/show)
- Staggered card grid
- 3D tilt on scroll
7. Variants Pattern for Stagger 8. Motion Value Events 9. Next.js & App Router Notes 10. Accessibility 11. Common Copilot Pitfalls
---
Package & Import Paths
npm install motion # recommended (renamed 2025)
npm install framer-motion # still works — same API// Recommended (Motion v12+)
import { motion, useScroll, useTransform, useSpring, useMotionValueEvent } from 'motion/react';
// Legacy — still valid
import { motion, useScroll, useTransform } from 'framer-motion';Motion v12 new features (2025):
- Hardware-accelerated scroll via browser ScrollTimeline API
useScrollandscroll()now GPU-accelerated by default- New color types:
oklch,oklab,color-mixanimatable directly - Full React 19 + concurrent rendering support
---
Two Types of Scroll Animation
Scroll-triggered (fires once when element enters viewport)
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.6, ease: [0.21, 0.47, 0.32, 0.98] }}
>
Content
</motion.div>viewport.margin — negative value triggers animation before element fully enters view. viewport.once — true means animate once, never reverse.
Scroll-linked (continuous, tied to scroll position)
const { scrollYProgress } = useScroll();
const opacity = useTransform(scrollYProgress, [0, 1], [0, 1]);
return <motion.div style={{ opacity }}>Content</motion.div>;The value updates on every scroll frame — must use style prop, not animate.
---
useScroll — Options Reference
const {
scrollX, // Absolute horizontal scroll (pixels)
scrollY, // Absolute vertical scroll (pixels)
scrollXProgress, // Horizontal progress 0→1 between offsets
scrollYProgress, // Vertical progress 0→1 between offsets
} = useScroll({
// Track a scrollable element instead of the viewport
container: containerRef,
// Track an element's position within the container
target: targetRef,
// Define when tracking starts and ends
// Format: ["target position container position", "target position container position"]
offset: ['start end', 'end start'],
// Common offset pairs:
// ['start end', 'end start'] = track while element is anywhere in view
// ['start end', 'end end'] = track from element entering to bottom of page
// ['start start', 'end start'] = track while element exits top
// ['center center', 'end start']= track from center-center to exit
// Update when content size changes (small perf cost, false by default)
trackContentSize: false,
});Offset string values:
start=0= top/left edgecenter=0.5= middleend=1= bottom/right edge- Numbers 0–1 also work:
[0, 1]=['start', 'end']
---
useTransform — Full Reference
// Map a motion value from one range to another
const y = useTransform(scrollYProgress, [0, 1], [0, -200]);
// Multi-stop interpolation
const opacity = useTransform(
scrollYProgress,
[0, 0.2, 0.8, 1],
[0, 1, 1, 0]
);
// Non-numeric values (colors, strings)
const color = useTransform(
scrollYProgress,
[0, 0.5, 1],
['#6366f1', '#ec4899', '#f97316']
);
// CSS string values
const clipPath = useTransform(
scrollYProgress,
[0, 1],
['inset(0% 100% 0% 0%)', 'inset(0% 0% 0% 0%)']
);
// Disable clamping (allow values outside output range)
const y = useTransform(scrollYProgress, [0, 1], [0, -200], { clamp: false });
// Transform from multiple inputs
const combined = useTransform(
[scrollX, scrollY],
([x, y]) => Math.sqrt(x * x + y * y)
);Rule: useTransform output is a MotionValue. It must go into the style prop of a motion.* element. Plain <div style={{ y }}> will NOT work — must be <motion.div style={{ y }}>.
---
useSpring for Smoothing
Wrap any MotionValue in useSpring to add spring physics — great for progress bars that feel alive.
const { scrollYProgress } = useScroll();
const smooth = useSpring(scrollYProgress, {
stiffness: 100, // Higher = faster/snappier response
damping: 30, // Higher = less bounce
restDelta: 0.001 // Precision threshold for stopping
});
return <motion.div style={{ scaleX: smooth }} />;For a subtle lag (not physics), use useTransform with clamp: false and an eased range instead.
---
Recipes with Copilot Prompts
1. Scroll Progress Bar
Copilot Chat Prompt:
Framer Motion: fixed scroll progress bar at top of page.
useScroll for page scroll progress, useSpring to smooth scaleX.
stiffness 100, damping 30. Grows left to right.'use client';
import { useScroll, useSpring, motion } from 'motion/react';
export function ScrollProgressBar() {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100, damping: 30, restDelta: 0.001,
});
return (
<motion.div
style={{ scaleX }}
className="fixed top-0 left-0 right-0 h-1 bg-indigo-500 origin-left z-50"
/>
);
}---
2. Reusable ScrollReveal Wrapper
Copilot Chat Prompt:
Framer Motion: reusable ScrollReveal component that wraps children with
fade-in-up entrance animation using whileInView. Props: delay (default 0),
duration (default 0.6), once (default true). viewport margin -80px.
TypeScript. 'use client'.'use client';
import { motion } from 'motion/react';
interface ScrollRevealProps {
children: React.ReactNode;
delay?: number;
duration?: number;
once?: boolean;
className?: string;
}
export function ScrollReveal({
children, delay = 0, duration = 0.6, once = true, className
}: ScrollRevealProps) {
return (
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once, margin: '-80px' }}
transition={{ duration, delay, ease: [0.21, 0.47, 0.32, 0.98] }}
className={className}
>
{children}
</motion.div>
);
}
// Usage:
// <ScrollReveal delay={0.2}><h2>Section Title</h2></ScrollReveal>---
3. Parallax Layers
Copilot Chat Prompt:
Framer Motion parallax section: background moves y from 0% to 30% (slow),
foreground text moves y from 50 to -50px (fast).
Both use target ref with offset ['start end', 'end start'].
Fade out at top and bottom using opacity useTransform [0, 0.3, 0.7, 1] → [0,1,1,0].'use client';
import { useRef } from 'react';
import { motion, useScroll, useTransform } from 'motion/react';
export function ParallaxSection() {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'end start'],
});
const backgroundY = useTransform(scrollYProgress, [0, 1], ['0%', '30%']);
const textY = useTransform(scrollYProgress, [0, 1], [50, -50]);
const opacity = useTransform(scrollYProgress, [0, 0.3, 0.7, 1], [0, 1, 1, 0]);
return (
<section ref={ref} className="relative h-screen overflow-hidden flex items-center justify-center">
<motion.div
className="absolute inset-0 bg-cover bg-center"
style={{ backgroundImage: 'url(/hero-bg.jpg)', y: backgroundY, scale: 1.2 }}
/>
<motion.div style={{ y: textY, opacity }} className="relative z-10 text-center text-white">
<h2 className="text-6xl font-bold">Parallax Title</h2>
<p className="text-xl mt-4">Scrolls at a different speed</p>
</motion.div>
</section>
);
}---
4. Horizontal Scroll Section
Copilot Chat Prompt:
Framer Motion horizontal scroll: 4 cards scroll horizontally as user scrolls vertically.
Outer container ref height 300vh controls speed (sticky pattern).
useScroll tracks outer container, useTransform maps scrollYProgress to x '0%' → '-75%'.'use client';
import { useRef } from 'react';
import { motion, useScroll, useTransform } from 'motion/react';
const cards = [
{ id: 1, title: 'Card One', color: 'bg-indigo-500' },
{ id: 2, title: 'Card Two', color: 'bg-pink-500' },
{ id: 3, title: 'Card Three', color: 'bg-amber-500' },
{ id: 4, title: 'Card Four', color: 'bg-teal-500' },
];
export function HorizontalScroll() {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ['start start', 'end end'],
});
const x = useTransform(scrollYProgress, [0, 1], ['0%', '-75%']);
return (
<div ref={containerRef} className="relative h-[300vh]">
<div className="sticky top-0 h-screen overflow-hidden">
<motion.div
style={{ x, width: `${cards.length * 100}vw` }}
className="flex gap-6 h-full items-center px-8"
>
{cards.map(card => (
<div
key={card.id}
className={`${card.color} w-screen h-[70vh] rounded-2xl flex items-center justify-center flex-shrink-0`}
>
<h3 className="text-white text-4xl font-bold">{card.title}</h3>
</div>
))}
</motion.div>
</div>
</div>
);
}---
5. Image Reveal with clipPath
Copilot Chat Prompt:
Framer Motion: image reveals left to right as it scrolls into view.
useScroll target ref, offset ['start end', 'center center'].
useTransform clipPath from 'inset(0% 100% 0% 0%)' to 'inset(0% 0% 0% 0%)'.
Also scale from 1.15 to 1.'use client';
import { useRef } from 'react';
import { motion, useScroll, useTransform } from 'motion/react';
export function ImageReveal({ src, alt }: { src: string; alt: string }) {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'center center'],
});
const clipPath = useTransform(
scrollYProgress,
[0, 1],
['inset(0% 100% 0% 0%)', 'inset(0% 0% 0% 0%)']
);
const scale = useTransform(scrollYProgress, [0, 1], [1.15, 1]);
return (
<div ref={ref} className="overflow-hidden rounded-xl">
<motion.img
src={src} alt={alt}
style={{ clipPath, scale }}
className="w-full h-full object-cover"
/>
</div>
);
}---
6. Scroll-linked Navbar (Hide on Scroll Down)
Copilot Chat Prompt:
Framer Motion navbar: transparent when at top, white with shadow after 80px.
Hide by sliding up when scrolling down, reveal when scrolling up.
Use useScroll, useMotionValueEvent to detect direction.
Animate y, backgroundColor, boxShadow with motion.nav.'use client';
import { useRef, useState } from 'react';
import { motion, useScroll, useMotionValueEvent } from 'motion/react';
export function Navbar() {
const { scrollY } = useScroll();
const [scrolled, setScrolled] = useState(false);
const [hidden, setHidden] = useState(false);
const prevRef = useRef(0);
useMotionValueEvent(scrollY, 'change', latest => {
const nextScrolled = latest > 80;
const nextHidden = latest > prevRef.current && latest > 200;
setScrolled(current => (current === nextScrolled ? current : nextScrolled));
setHidden(current => (current === nextHidden ? current : nextHidden));
prevRef.current = latest;
});
return (
<motion.nav
animate={{
y: hidden ? -80 : 0,
backgroundColor: scrolled ? 'rgba(255,255,255,0.95)' : 'rgba(255,255,255,0)',
boxShadow: scrolled ? '0 1px 24px rgba(0,0,0,0.08)' : 'none',
}}
transition={{ duration: 0.3, ease: 'easeInOut' }}
className="fixed top-0 left-0 right-0 z-50 backdrop-blur-sm"
>
{/* nav links */}
</motion.nav>
);
}---
7. Staggered Card Grid
Copilot Chat Prompt:
Framer Motion: card grid with stagger entrance. Use variants:
container has staggerChildren 0.1, delayChildren 0.2.
Each card: hidden (opacity 0, y 40, scale 0.96) → visible (opacity 1, y 0, scale 1).
Trigger with whileInView on the container. Once.'use client';
import { motion } from 'motion/react';
const containerVariants = {
hidden: {},
visible: {
transition: { staggerChildren: 0.1, delayChildren: 0.2 }
}
};
const cardVariants = {
hidden: { opacity: 0, y: 40, scale: 0.96 },
visible: {
opacity: 1, y: 0, scale: 1,
transition: { duration: 0.5, ease: [0.21, 0.47, 0.32, 0.98] }
}
};
export function CardGrid({ cards }: { cards: { id: number; title: string }[] }) {
return (
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: '-50px' }}
className="grid grid-cols-3 gap-6"
>
{cards.map(card => (
<motion.div key={card.id} variants={cardVariants}
className="bg-white rounded-xl p-6 shadow-sm border"
>
<h3>{card.title}</h3>
</motion.div>
))}
</motion.div>
);
}---
8. 3D Tilt on Scroll
Copilot Chat Prompt:
Framer Motion: 3D perspective card that rotates on X axis as it scrolls through view.
rotateX 15→0→-15, scale 0.9→1→0.9, opacity 0→1→0.
Target ref with offset ['start end', 'end start']. Wrap in perspective container.'use client';
import { useRef } from 'react';
import { motion, useScroll, useTransform } from 'motion/react';
export function TiltCard({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'end start'],
});
const rotateX = useTransform(scrollYProgress, [0, 0.5, 1], [15, 0, -15]);
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.9, 1, 0.9]);
const opacity = useTransform(scrollYProgress, [0, 0.2, 0.8, 1], [0, 1, 1, 0]);
return (
<div ref={ref} style={{ perspective: '1000px' }}>
<motion.div
style={{ rotateX, scale, opacity }}
className="bg-white rounded-2xl p-8 shadow-lg"
>
{children}
</motion.div>
</div>
);
}---
Variants Pattern for Stagger
Variants propagate automatically from parent to children — you don't need to pass them down manually.
const parent = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.1, // Delay between each child
delayChildren: 0.2, // Initial delay before first child
when: 'beforeChildren', // Parent animates before children
}
}
};
const child = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5 } }
};
// Children with `variants={child}` automatically get the stagger
// when the parent transitions between 'hidden' and 'visible'---
Motion Value Events
import { useScroll, useMotionValueEvent } from 'motion/react';
const { scrollY } = useScroll();
// Fires on every change — use for imperative side effects
useMotionValueEvent(scrollY, 'change', latest => {
console.log('scroll position:', latest);
});
// Detect scroll direction
const [direction, setDirection] = useState<'up' | 'down'>('down');
useMotionValueEvent(scrollY, 'change', current => {
const diff = current - scrollY.getPrevious()!;
setDirection(diff > 0 ? 'down' : 'up');
});When to use `useMotionValueEvent` vs `useTransform`:
- Use
useTransformwhen you want a CSS value that animates smoothly (y, opacity, color) - Use
useMotionValueEventwhen you want to fire React state changes or side effects
---
Next.js & App Router Notes
// Every file using motion hooks must be a Client Component
'use client';
// For page-level scroll tracking in App Router, use useScroll in a layout
// that's already a client component — don't try to use it in Server Components
// If you need SSR-safe scroll animations, gate with:
import { useEffect, useState } from 'react';
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null; // or a skeletonRecommended pattern for Next.js App Router: 1. Keep all motion.* components in separate 'use client' files 2. Import them into Server Components — they'll be client-rendered automatically 3. Use AnimatePresence at the layout level for page transitions
---
Accessibility
import { useReducedMotion } from 'motion/react';
export function AnimatedCard() {
const prefersReducedMotion = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: prefersReducedMotion ? 0 : 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: prefersReducedMotion ? 0 : 0.6 }}
>
Content
</motion.div>
);
}Or disable all scroll-linked transforms when reduced motion is preferred:
const prefersReducedMotion = useReducedMotion();
const y = useTransform(
scrollYProgress, [0, 1],
prefersReducedMotion ? [0, 0] : [100, -100] // no movement if reduced motion
);---
Common Copilot Pitfalls
Missing 'use client': Copilot forgets to add this for Next.js App Router files. Every file using useScroll, useTransform, motion.*, or any hook needs 'use client' at the top.
Using style prop on a plain div: Copilot sometimes writes <div style={{ y }}> where y is a MotionValue. This silently does nothing. Must be <motion.div style={{ y }}>.
Old import path: Copilot still generates from 'framer-motion' (valid, but legacy). Current canonical: from 'motion/react'.
Forgetting offset on useScroll: Without offset, scrollYProgress tracks the full page from 0 to 1 — not the element's position. Always pass target + offset for element-level tracking.
Missing ref on target: Copilot sometimes writes target: ref but forgets to attach ref to the DOM element.
const ref = useRef(null);
const { scrollYProgress } = useScroll({ target: ref }); // ← ref passed
return <div ref={ref}>...</div>; // ← ref attachedUsing animate prop for scroll-linked values: Scroll-linked values must use style, not animate. animate runs on mount/unmount, not on scroll.
// ❌ Wrong
<motion.div animate={{ opacity }} />
// ✅ Correct
<motion.div style={{ opacity }} />Not smoothing scroll progress: Raw scrollYProgress can feel mechanical on fine motion. Wrap in useSpring for progress bars and UI elements that need a polished feel.
GSAP ScrollTrigger — Full Reference
Table of Contents
1. Installation & Registration 2. ScrollTrigger Config Reference 3. Start / End Syntax Decoded 4. toggleActions Values 5. Recipes with Copilot Prompts
- Fade-in batch reveal
- Scrub animation
- Pinned timeline
- Parallax layers
- Horizontal scroll
- Character stagger text
- Scroll snap
- Progress bar
- ScrollSmoother
- Scroll counter
6. React Integration (useGSAP) 7. Lenis Smooth Scroll 8. Responsive with matchMedia 9. Accessibility 10. Performance & Cleanup 11. Common Copilot Pitfalls
---
Installation & Registration
npm install gsap
# React
npm install gsap @gsap/reactimport gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { ScrollSmoother } from 'gsap/ScrollSmoother'; // optional
gsap.registerPlugin(ScrollTrigger, ScrollSmoother);CDN (vanilla):
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/ScrollTrigger.min.js"></script>---
ScrollTrigger Config Reference
gsap.to('.element', {
x: 500,
ease: 'none', // Use 'none' for scrub animations
scrollTrigger: {
trigger: '.section', // Element whose position triggers the animation
start: 'top 80%', // "[trigger edge] [viewport edge]"
end: 'bottom 20%', // Where animation ends
scrub: 1, // Link progress to scroll; use true for instant scrub, or a number for smooth lag
pin: true, // Pin trigger element during scroll; use a selector/element to pin something else
pinSpacing: true, // Add space below pinned element (default: true)
markers: true, // Debug markers — REMOVE in production
toggleActions: 'play none none reverse', // onEnter onLeave onEnterBack onLeaveBack
toggleClass: 'active', // CSS class added/removed when active
snap: { snapTo: 'labels', duration: 0.3, ease: 'power1.inOut' }, // Or use a number like 1 to snap to increments
fastScrollEnd: true, // Force completion if user scrolls past fast
horizontal: false, // true for horizontal scroll containers
anticipatePin: 1, // Reduces pin jump (seconds to anticipate)
invalidateOnRefresh: true, // Recalculate positions on resize
id: 'my-trigger', // For ScrollTrigger.getById()
onEnter: () => {},
onLeave: () => {},
onEnterBack: () => {},
onLeaveBack: () => {},
onUpdate: self => console.log(self.progress), // 0 to 1
onToggle: self => console.log(self.isActive),
}
});---
Start / End Syntax Decoded
Format: "[trigger position] [viewport position]"
| Value | Meaning |
|---|---|
"top bottom" | Top of trigger hits bottom of viewport — enters view |
"top 80%" | Top of trigger reaches 80% down from top of viewport |
"top center" | Top of trigger reaches viewport center |
"top top" | Top of trigger at top of viewport |
"center center" | Centers align |
"bottom top" | Bottom of trigger at top of viewport — exits view |
"+=200" | 200px after trigger position |
"-=100" | 100px before trigger position |
"+=200%" | 200% of viewport height after trigger |
---
toggleActions Values
toggleActions: "play pause resume reset"
^ ^ ^ ^
onEnter onLeave onEnterBack onLeaveBack| Value | Effect |
|---|---|
play | Play from current position |
pause | Pause at current position |
resume | Resume from where paused |
reverse | Play backwards |
reset | Jump to start |
restart | Play from beginning |
none | Do nothing |
Most common for entrance animations: "play none none none" (animate once, don't reverse).
---
Recipes with Copilot Prompts
1. Fade-in Batch Reveal
Copilot Chat Prompt:
Using GSAP ScrollTrigger.batch, animate all .card elements:
fade in from opacity 0, y 50 when they enter the viewport at 85%.
Stagger 0.15s between cards. Animate once (no reverse).gsap.registerPlugin(ScrollTrigger);
ScrollTrigger.batch('.card', {
onEnter: elements => {
gsap.from(elements, {
opacity: 0,
y: 50,
stagger: 0.15,
duration: 0.8,
ease: 'power2.out',
});
},
start: 'top 85%',
});Why batch over individual ScrollTriggers: batch groups elements entering together into one animation call, which is more performant than creating one ScrollTrigger per element.
---
2. Scrub Animation (scroll-linked)
Copilot Chat Prompt:
GSAP scrub: animate .hero-image scale from 1 to 1.3 and opacity to 0
as the user scrolls past .hero-section.
Perfectly synced to scroll position, no pin.gsap.to('.hero-image', {
scale: 1.3,
opacity: 0,
ease: 'none', // Critical: linear easing for scrub
scrollTrigger: {
trigger: '.hero-section',
start: 'top top',
end: 'bottom top',
scrub: true,
}
});---
3. Pinned Timeline
Copilot Chat Prompt:
GSAP pinned timeline: pin .story-section while a sequence plays —
fade in .title (y: 60), scale .image to 1, slide .text from x: 80.
Total scroll distance 300vh. Scrub 1 for smoothness.const tl = gsap.timeline({
scrollTrigger: {
trigger: '.story-section',
start: 'top top',
end: '+=300%',
pin: true,
scrub: 1,
anticipatePin: 1,
}
});
tl
.from('.title', { opacity: 0, y: 60, duration: 1 })
.from('.image', { scale: 0.85, opacity: 0, duration: 1 }, '-=0.3')
.from('.text', { x: 80, opacity: 0, duration: 1 }, '-=0.3');---
4. Parallax Layers
Copilot Chat Prompt:
GSAP parallax: background image moves yPercent -20 (slow),
foreground text moves yPercent -60 (fast). Both scrubbed to scroll, no pin.
Trigger is .parallax-section, start top bottom, end bottom top.// Slow background
gsap.to('.parallax-bg', {
yPercent: -20,
ease: 'none',
scrollTrigger: {
trigger: '.parallax-section',
start: 'top bottom',
end: 'bottom top',
scrub: true,
}
});
// Fast foreground
gsap.to('.parallax-fg', {
yPercent: -60,
ease: 'none',
scrollTrigger: {
trigger: '.parallax-section',
start: 'top bottom',
end: 'bottom top',
scrub: true,
}
});---
5. Horizontal Scroll Section
Copilot Chat Prompt:
GSAP horizontal scroll: 4 .panel elements inside .panels-container.
Pin .horizontal-section, scrub 1, snap per panel.
End should use offsetWidth so it recalculates on resize.const sections = gsap.utils.toArray('.panel');
gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
ease: 'none',
scrollTrigger: {
trigger: '.horizontal-section',
pin: true,
scrub: 1,
snap: 1 / (sections.length - 1),
end: () => `+=${document.querySelector('.panels-container').offsetWidth}`,
invalidateOnRefresh: true,
}
});Required HTML:
<div class="horizontal-section">
<div class="panels-container">
<div class="panel">1</div>
<div class="panel">2</div>
<div class="panel">3</div>
<div class="panel">4</div>
</div>
</div>Required CSS:
.horizontal-section { overflow: hidden; }
.panels-container { display: flex; flex-wrap: nowrap; width: 400vw; }
.panel { width: 100vw; height: 100vh; flex-shrink: 0; }---
6. Character Stagger Text Reveal
Copilot Chat Prompt:
Split .hero-title into characters using SplitType.
Animate each char: opacity 0→1, y 80→0, rotateX -90→0.
Stagger 0.03s, ease back.out(1.7). Trigger when heading enters at 85%.npm install split-typeimport SplitType from 'split-type';
const text = new SplitType('.hero-title', { types: 'chars' });
gsap.from(text.chars, {
opacity: 0,
y: 80,
rotateX: -90,
stagger: 0.03,
duration: 0.6,
ease: 'back.out(1.7)',
scrollTrigger: {
trigger: '.hero-title',
start: 'top 85%',
toggleActions: 'play none none none',
}
});---
7. Scroll Snap Sections
Copilot Chat Prompt:
GSAP: each full-height section scales from 0.9 to 1 when it enters view.
Also add global scroll snapping between sections using ScrollTrigger.create snap.const sections = gsap.utils.toArray('section');
sections.forEach(section => {
gsap.from(section, {
scale: 0.9,
opacity: 0.6,
scrollTrigger: {
trigger: section,
start: 'top 90%',
toggleActions: 'play none none reverse',
}
});
});
ScrollTrigger.create({
snap: {
snapTo: (progress) => {
const step = 1 / (sections.length - 1);
return Math.round(progress / step) * step;
},
duration: { min: 0.2, max: 0.5 },
ease: 'power1.inOut',
}
});---
8. Scroll Progress Bar
Copilot Chat Prompt:
GSAP: fixed progress bar at top of page. scaleX 0→1 linked to
full page scroll, scrub 0.3 for slight smoothing. transformOrigin left center.gsap.to('.progress-bar', {
scaleX: 1,
ease: 'none',
transformOrigin: 'left center',
scrollTrigger: {
trigger: document.body,
start: 'top top',
end: 'bottom bottom',
scrub: 0.3,
}
});.progress-bar {
position: fixed; top: 0; left: 0;
width: 100%; height: 4px;
background: #6366f1;
transform-origin: left;
transform: scaleX(0);
z-index: 999;
}---
9. ScrollSmoother Setup
Copilot Chat Prompt:
Set up GSAP ScrollSmoother with smooth: 1.5, effects: true.
Show the required wrapper HTML structure.
Add data-speed and data-lag to parallax elements.# ScrollSmoother is part of gsap — no extra install neededimport { ScrollSmoother } from 'gsap/ScrollSmoother';
gsap.registerPlugin(ScrollTrigger, ScrollSmoother);
ScrollSmoother.create({
wrapper: '#smooth-wrapper',
content: '#smooth-content',
smooth: 1.5,
effects: true,
smoothTouch: 0.1,
});<div id="smooth-wrapper">
<div id="smooth-content">
<img data-speed="0.5" src="bg.jpg" /> <!-- 50% scroll speed -->
<div data-lag="0.3" class="float">...</div> <!-- 0.3s lag -->
</div>
</div>---
10. Animated Number Counter
Copilot Chat Prompt:
GSAP: animate .counter elements from 0 to their data-target value
when they enter the viewport. Duration 2s, ease power2.out.
Format with toLocaleString. Animate once.document.querySelectorAll('.counter').forEach(el => {
const obj = { val: 0 };
gsap.to(obj, {
val: parseInt(el.dataset.target, 10),
duration: 2,
ease: 'power2.out',
onUpdate: () => { el.textContent = Math.round(obj.val).toLocaleString(); },
scrollTrigger: {
trigger: el,
start: 'top 85%',
toggleActions: 'play none none none',
}
});
});<span class="counter" data-target="12500">0</span>---
React Integration (useGSAP)
npm install gsap @gsap/reactimport { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(useGSAP, ScrollTrigger);Why useGSAP instead of useEffect: useGSAP automatically kills all ScrollTriggers created inside it when the component unmounts — preventing memory leaks. It also handles React strict mode's double-invoke correctly. Think of it as a drop-in replacement for useLayoutEffect that GSAP understands.
Copilot Chat Prompt:
React: use useGSAP from @gsap/react to animate .card elements inside containerRef.
Fade in from y 60, opacity 0, stagger 0.12, scrollTrigger start top 80%.
Scope to containerRef so selectors don't match outside this component.export function AnimatedSection() {
const containerRef = useRef(null);
useGSAP(() => {
gsap.from('.card', {
opacity: 0,
y: 60,
stagger: 0.12,
duration: 0.7,
ease: 'power2.out',
scrollTrigger: {
trigger: containerRef.current,
start: 'top 80%',
toggleActions: 'play none none none',
}
});
}, { scope: containerRef });
return (
<div ref={containerRef}>
<div className="card">One</div>
<div className="card">Two</div>
</div>
);
}Pinned timeline in React:
export function PinnedStory() {
const sectionRef = useRef(null);
useGSAP(() => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: sectionRef.current,
pin: true, scrub: 1,
start: 'top top', end: '+=200%',
}
});
tl.from('.story-title', { opacity: 0, y: 40 })
.from('.story-image', { scale: 0.85, opacity: 0 }, '-=0.2')
.from('.story-text', { opacity: 0, x: 40 }, '-=0.2');
}, { scope: sectionRef });
return (
<section ref={sectionRef}>
<h2 className="story-title">Chapter One</h2>
<img className="story-image" src="/photo.jpg" alt="" />
<p className="story-text">The story begins.</p>
</section>
);
}Next.js note: Run gsap.registerPlugin(ScrollTrigger) inside a useGSAP or useLayoutEffect — or guard it:
if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger);---
Lenis Smooth Scroll
npm install lenisCopilot Chat Prompt:
Integrate Lenis smooth scroll with GSAP ScrollTrigger.
Add lenis.raf to gsap.ticker. Set lagSmoothing to 0.
Destroy lenis on unmount if in React.import Lenis from 'lenis';
import { useEffect } from 'react';
const lenis = new Lenis({ duration: 1.2, smoothWheel: true });
const raf = (time) => lenis.raf(time * 1000);
gsap.ticker.add(raf);
gsap.ticker.lagSmoothing(0);
lenis.on('scroll', ScrollTrigger.update);
// React cleanup
useEffect(() => {
return () => {
lenis.destroy();
gsap.ticker.remove(raf);
};
}, []);---
Responsive with matchMedia
Copilot Chat Prompt:
Use gsap.matchMedia to animate x: 200 on desktop (min-width: 768px)
and y: 100 on mobile. Both should skip animation if prefers-reduced-motion is set.const mm = gsap.matchMedia();
mm.add({
isDesktop: '(min-width: 768px)',
isMobile: '(max-width: 767px)',
noMotion: '(prefers-reduced-motion: reduce)',
}, context => {
const { isDesktop, isMobile, noMotion } = context.conditions;
if (noMotion) return;
gsap.from('.box', {
x: isDesktop ? 200 : 0,
y: isMobile ? 100 : 0,
opacity: 0,
scrollTrigger: { trigger: '.box', start: 'top 80%' }
});
});---
Accessibility
// Guard all scroll animations with prefers-reduced-motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
if (!prefersReducedMotion.matches) {
gsap.from('.box', {
opacity: 0, y: 50,
scrollTrigger: { trigger: '.box', start: 'top 85%' }
});
} else {
// Show element immediately, no animation
gsap.set('.box', { opacity: 1, y: 0 });
}Or use gsap.matchMedia() with prefers-reduced-motion: reduce condition (see above).
---
Performance & Cleanup
// Kill a specific trigger
const st = ScrollTrigger.create({ ... });
st.kill();
// Kill all triggers (e.g., on page transition)
ScrollTrigger.killAll();
// Refresh all trigger positions (after dynamic content loads)
ScrollTrigger.refresh();Performance rules:
- Only animate
transformandopacity— GPU-accelerated, no layout recalculation - Avoid animating
width,height,top,left,box-shadow,filter - Use
ScrollTrigger.batch()for many similar elements — far better than one trigger per element - Add
will-change: transformsparingly — only on actively animating elements - Always remove
markers: truebefore production
---
Common Copilot Pitfalls
Forgot registerPlugin: Copilot often omits gsap.registerPlugin(ScrollTrigger). Always add it before any ScrollTrigger usage.
Wrong ease for scrub: Copilot defaults to power2.out even on scrub animations. Always use ease: 'none' when scrub: true or scrub: number.
useEffect instead of useGSAP in React: Copilot generates useEffect — always swap to useGSAP.
Static end value for horizontal scroll: Copilot writes end: "+=" + container.offsetWidth. Correct: end: () => "+=" + container.offsetWidth (function form recalculates on resize).
markers left in production: Copilot adds markers: true and leaves it. Always remove.
Scrub without pin on long animations: Scrubbing a long timeline without pinning means the element scrolls out of view. Add pin: true or shorten the scroll distance.
Related skills
How it compares
Choose gsap-framer-scroll-animation for React scroll-linked Motion hooks; pick GSAP-specific skills when timelines and non-React DOM animation are required.
FAQ
What is gsap-framer-scroll-animation?
>-
When should I use gsap-framer-scroll-animation?
>-
Is gsap-framer-scroll-animation safe to install?
Review the Security Audits panel on this page before production use.