GSAP Scroll Animations That Actually Feel Good
Published
13 min read
Apr 15, 2025

Design

GSAP Scroll Animations That Actually Feel Good

GSAPAnimationDesignScrollTrigger

Overview

Most scroll animations feel cheap. Here's the exact easing, timing, and trigger logic we use to make motion feel premium and intentional.

GSAPAnimationDesignScrollTrigger
Sana Yildiz

Sana Yildiz

Author
13 min read
Reading Time

GSAP Scroll Animations That Actually Feel Good

Why Most Scroll Animations Feel Cheap

The web is full of scroll animations that make me motion-sick. Elements fly in from nowhere, parallax layers jitter, and nothing feels connected to the user's scroll position.

The problem isn't GSAP. The problem is treating animation as decoration instead of communication.

GSAP scroll animation exampleGSAP scroll animation example

The Physics of Good Scroll Animation

Good animation mimics the physical world. Scroll position becomes a slider that controls animation progress — not a trigger for abrupt changes.

Easing That Feels Natural

javascript
// Bad: Linear easing feels robotic
gsap.to(element, {
  scrollTrigger: {
    scrub: true
  },
  x: 500
})

// Good: Power easing creates momentum
gsap.to(element, {
  scrollTrigger: {
    scrub: true,
    ease: "power2.out" // Slow start, fast end
  },
  x: 500
})

ScrollTrigger: The Right Way

Basic Setup That Doesn't Suck

javascript
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'

gsap.registerPlugin(ScrollTrigger)

// The professional baseline
const animation = gsap.from('.hero-title', {
  scrollTrigger: {
    trigger: '.hero',
    start: 'top bottom', // When trigger's top hits bottom of viewport
    end: 'top center',   // When trigger's top hits center
    scrub: 0.5,          // 0.5 second lag for smoothness
    markers: false,      // Only enable during debugging
    invalidateOnRefresh: true // Responsive-friendly
  },
  y: 100,
  opacity: 0,
  duration: 1,
  ease: 'power2.out'
})

ScrollTrigger visualizationScrollTrigger visualization

The Three Scroll Patterns That Work

Pattern 1: The Reveal

Elements fade in as they enter viewport:

javascript
gsap.utils.toArray('.reveal').forEach(element => {
  gsap.from(element, {
    scrollTrigger: {
      trigger: element,
      start: 'top 85%', // When element's top is 85% down viewport
      toggleActions: 'play none none reverse'
    },
    y: 50,
    opacity: 0,
    duration: 0.8,
    ease: 'power2.out'
  })
})

Pattern 2: The Parallax (Without Jitter)

javascript
// Background moves slower than foreground
gsap.to('.parallax-bg', {
  scrollTrigger: {
    trigger: '.section',
    start: 'top bottom',
    end: 'bottom top',
    scrub: true
  },
  y: (i, target) => -target.offsetHeight * 0.2
})

// Foreground moves faster
gsap.to('.parallax-fg', {
  scrollTrigger: {
    trigger: '.section',
    start: 'top bottom',
    end: 'bottom top',
    scrub: true
  },
  y: (i, target) => target.offsetHeight * 0.3
})

Pattern 3: The Progress Indicator

A common but often badly implemented pattern:

javascript
gsap.to('.progress-bar', {
  scrollTrigger: {
    trigger: document.body,
    start: 'top top',
    end: 'bottom bottom',
    scrub: 0.3
  },
  scaleX: 1,
  transformOrigin: '0% 50%',
  ease: 'none'
})

Progress indicator animationProgress indicator animation

Advanced Techniques for Premium Feel

The Stagger Effect (But Make It Good)

javascript
// Bad: All elements animate identically
gsap.from('.cards', {
  scrollTrigger: {
    trigger: '.cards-container',
    start: 'top 80%'
  },
  scale: 0
})

// Good: Stagger with curve
gsap.from('.card', {
  scrollTrigger: {
    trigger: '.cards-container',
    start: 'top 80%'
  },
  scale: 0,
  opacity: 0,
  stagger: {
    each: 0.1,
    from: 'center',
    ease: 'power1.inOut'
  },
  duration: 0.6,
  ease: 'back.out(0.7)'
})

Pin and Snap (For Narrative Scrollytelling)

javascript
ScrollTrigger.create({
  trigger: '.story-section',
  start: 'top top',
  end: '+=300%',
  pin: true,
  snap: {
    snapTo: [0, 0.33, 0.66, 1],
    duration: 0.5,
    ease: 'power1.inOut'
  },
  onUpdate: (self) => {
    // Update chapter indicators
    document.querySelector('.chapter').textContent = 
      Math.floor(self.progress * 4) + 1
  }
})

Performance That Doesn't Jank

The 60fps Checklist

Use will-change for animating properties ✅ Animate transforms and opacity only (avoid layout properties) ✅ Batch animations when possible ✅ Kill ScrollTrigger on route changes

css
/* Tell browser to optimize */
.animated-element {
  will-change: transform, opacity;
}
javascript
// Clean up to prevent memory leaks
useEffect(() => {
  const animations = []
  
  animations.push(gsap.from('.hero', { /* ... */ }))
  
  return () => {
    animations.forEach(anim => anim.kill())
    ScrollTrigger.getAll().forEach(trigger => trigger.kill())
  }
}, [])

The Accessibility Layer

Not everyone appreciates scroll animations:

javascript
// Respect prefers-reduced-motion
const shouldAnimate = window.matchMedia('(prefers-reduced-motion: no-preference)').matches

if (shouldAnimate) {
  // Your scroll animations
  gsap.from('.element', { /* ... */ })
} else {
  // Just show everything statically
  gsap.set('.element', { opacity: 1, y: 0 })
}

Real Project Examples

Hero Section That Works

javascript
// Complete hero animation
const tl = gsap.timeline({
  scrollTrigger: {
    trigger: '.hero',
    start: 'top top',
    end: 'bottom top',
    scrub: 0.5,
    pin: true
  }
})

tl.from('.hero-title', { scale: 1.2, opacity: 0, duration: 2 })
  .from('.hero-subtitle', { y: 50, opacity: 0 }, '-=1.5')
  .from('.hero-cta', { y: 30, opacity: 0 }, '-=1')
  .to('.hero-overlay', { opacity: 0.8 }, 0)

The "Don't List" (What Proves You're an Amateur)

❌ Animating top/left/width/height (causes layout recalc) ❌ Using scrollTo without easing ❌ Ignoring scrub values (0 gives abrupt changes) ❌ Creating 50 independent ScrollTriggers (use timelines) ❌ No cleanup on route changes

Progressive Enhancement Flow

  1. Build without animations (core content works)
  2. Add CSS transitions (basic polish)
  3. Layer GSAP (premium feel)
  4. Test at 3x scroll speed (still feels good)
  5. Validate reduced-motion (accessible)

The best scroll animations are the ones users don't notice — they just feel right. That's the goal. Not "look what I can do," but "this experience is delightful."

Sana Yildiz

Written by

Sana Yildiz

Let's work together

Start your project.