All skills
github avatar

/gsap-framer-scroll-animation

@66a60af official
by githubgithub/awesome-copilot40k stars
5,040

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. Covers GSAP ScrollTrigger (pinning, scrubbing, snapping, timelines, horizontal scroll, ScrollSmoother, matchMedia) and Framer Motion / Motion v12 (useScroll, useTransform, useSpring, whileInView, variants). Use this skill even if the user just says "animate on scroll", "fade in as I scroll", "make it scroll like Apple", "parallax effect", "sticky section", "scroll progress bar", or "entrance animation". Also triggers for Copilot prompt patterns for GSAP or Framer Motion code generation. Pairs with the premium-frontend-ui skill for creative philosophy and design-level polish.

Use this Skill: https://skilld.dev/gh/github/awesome-copilot/gsap-framer-scroll-animation

This session only. Nothing lands on disk.

referencesgsap.md

≈4.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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/react
import 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-type
import 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 needed
import { 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/react
import { 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 lenis

Copilot 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 transform and opacity — 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: transform sparingly — only on actively animating elements
  • Always remove markers: true before 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.

Source: SKILL.md on GitHub

No alerts16d4 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides configuration guidelines, code recipes, and reference implementations for building scroll animations using GSAP and Framer Motion. No security issues or malicious patterns were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 66a60af. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 6 months ago
Other metadata
metadata
{
  "author": "Utkarsh Patrikar",
  "author_url": "https://github.com/utkarsh232005"
}
  • React
  • Next.js
  • gsap
  • framer-motion
  • scroll-animations
  • scrolltrigger
  • parallax
  • motion
  • uiscroll

README badge

README badge for github/awesome-copilot/gsap-framer-scroll-animation

Implements scroll-triggered animations using GSAP ScrollTrigger or Framer Motion, covering pinning, scrubbing, parallax, fade-in reveals, and horizontal scroll. Choose GSAP for vanilla JS or complex timelines; use Framer Motion for React/Next.js with declarative hooks like useScroll and whileInView.

Generated from the current SKILL.md.

Does this skill cover both GSAP and Framer Motion?
Yes. The skill covers GSAP ScrollTrigger (pinning, scrubbing, timelines, horizontal scroll) and Framer Motion / Motion v12 (useScroll, useTransform, whileInView). Choose based on your stack: GSAP for vanilla JS or complex timelines; Framer Motion for React/Next.js with declarative style.
Do I need to install anything beyond GSAP or Framer Motion?
For GSAP in React, install @gsap/react to use the useGSAP hook, which auto-cleans ScrollTriggers. Framer Motion (or the newer 'motion' package) includes all scroll hooks built-in. Basic GSAP requires no additional packages.
What about Next.js — which library should I use?
Either works in Next.js. Use Framer Motion for declarative, hook-based scroll animations with 'use client' at the top of your file. Use GSAP with useGSAP hook for more complex timelines and pinning. The skill covers both.
Does this handle accessibility for reduced motion?
The skill emphasizes checking prefers-reduced-motion. Full patterns for respecting it are in the reference files (gsap.md and framer.md).
Can I use this skill if I only know vanilla JavaScript?
Yes. GSAP is the best fit for vanilla JS, Webflow, and Vue. The skill includes vanilla JS recipes and Copilot prompts for scroll animations without React.

Generated from the current SKILL.md. These answers refresh after source changes.