Front-End Engineering

GSAP & Framer Motion Scroll Animation

The complete, engineer-grade guide to building scroll animation with GSAP ScrollTrigger and Framer Motion in Next.js — when to reach for each, working code, and the Core Web Vitals rules that keep motion from wrecking your LCP.

By Sean Guillermo · Updated July 11, 2026

What scroll animation actually is

Scroll animation is motion driven by the reader's scroll position rather than by a click or a timer. It comes in two structurally different forms, and confusing them is the source of most janky, slow, or inaccessible implementations. Getting the distinction right is the first design decision, because it determines which library and which API you should use.

Scroll-triggered vs scroll-linked

  • Scroll-triggered animation fires once when an element crosses into the viewport — a card fades and lifts into place, then stays. It is a discrete event. In Framer Motion this is the whileInView prop; in GSAP it is a ScrollTrigger with no scrub.
  • Scroll-linked animation binds progress directly to scroll position, so scrolling scrubs the timeline forward and backward like a video scrubber. This powers parallax, horizontal galleries, and pinned storytelling. In Framer Motion it is useScroll piped through useTransform; in GSAP it is scrub, usually combined with pin.

The rule of thumb: if the effect should feel like a reveal, use scroll-triggered. If the effect should feel like the user is physically dragging the animation, use scroll-linked. Scroll-linked effects are more expensive and more likely to cause motion sickness, so they demand stricter performance and accessibility discipline.

When to use GSAP ScrollTrigger vs Framer Motion

Both libraries can produce nearly any effect, so the decision is about ergonomics, control, and weight — not raw capability. The heuristic below reflects how each tool is architected.

Choose GSAP ScrollTrigger when…

  • You need precise, multi-step timelines where dozens of tweens are choreographed against one scroll range.
  • You need pinning — freezing a section in place while its contents animate — which GSAP handles natively and robustly.
  • The project is not React-only, or you want the same animation engine across React, vanilla, and other frameworks.
  • You want advanced effects like SVG morphing or text splitting, now free via the former Club plugins.

Choose Framer Motion when…

  • You are building a React or Next.js app and want a declarative API that lives inside your component tree.
  • Most of your motion is component-level — reveals, hovers, layout transitions, and simple parallax.
  • You value the built-in useReducedMotion hook and viewport controls that make accessible, one-shot reveals trivial.
  • You want less setup: no plugin registration, no manual cleanup boilerplate beyond React's own lifecycle.

GSAP vs Framer Motion: direct comparison

DimensionGSAP + ScrollTriggerFramer Motion
Bundle sizeCore is light; ScrollTrigger adds ~40KB+ min. Tree-shake per plugin.Larger baseline; the "m" component and LazyMotion trim it substantially.
React integrationFramework-agnostic; runs in useEffect or the useGSAP hook.React-native; declarative props and hooks, first-class in Next.js.
Timeline controlBest-in-class: nested timelines, labels, precise sequencing.Good for simple sequences; complex choreography is harder.
PinningNative, battle-tested pin + scrub.No native pin; requires sticky CSS plus manual math.
Learning curveSteeper; imperative API and many options.Gentler for React devs; declarative and intuitive.
Licensing100% free incl. all former Club plugins since Webflow 2024 acquisition.Open source (MIT), free for commercial use.
SSR / Next.js fitClient-only; guard in useEffect, register plugins once.Client-only components; pairs cleanly with Server Components.

The headline licensing change matters: for years, GSAP's premium ScrollTrigger add-ons like ScrollSmoother sat behind a paid Club membership. Following Webflow's 2024 acquisition of GreenSock, GSAP and every former Club plugin became 100% free, including for commercial and client work. That removes the last real reason many React teams defaulted to Framer Motion purely on cost.

GSAP ScrollTrigger example: pin and scrub

This pattern pins a section in place and scrubs a horizontal move as the user scrolls — the canonical scroll-linked effect. Note the motion-preference guard, and the ctx.revert()cleanup that tears down triggers on unmount to prevent leaks and layout thrash.

'use client';
import { useRef, useEffect } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

export function PinnedPanel() {
  const root = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // Respect user motion preferences before doing any heavy scrubbing.
    const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
    if (reduce.matches) return;

    const ctx = gsap.context(() => {
      gsap.to('.panel-inner', {
        xPercent: -50,           // scroll-linked horizontal move
        ease: 'none',
        scrollTrigger: {
          trigger: '.panel',
          start: 'top top',
          end: '+=1200',
          pin: true,             // freeze the section while it animates
          scrub: 1,              // tie progress to scroll position
        },
      });
    }, root);

    return () => ctx.revert();   // clean up triggers on unmount
  }, []);

  return (
    <div ref={root} className="panel">
      <div className="panel-inner">/* wide content */</div>
    </div>
  );
}

Framer Motion example: useScroll + useTransform

The same concepts, declaratively. useScroll reports this section's progress through the viewport, useTransform maps that progress to a parallax offset, and whileInView handles a one-shot reveal for the heading. The useReducedMotionhook collapses the parallax range to zero when the user asks for less motion.

'use client';
import { useRef } from 'react';
import { motion, useScroll, useTransform, useReducedMotion } from 'motion/react';

export function ParallaxSection() {
  const ref = useRef<HTMLDivElement>(null);
  const reduce = useReducedMotion();

  // Track this element's progress through the viewport (0 to 1).
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ['start end', 'end start'],
  });

  // Map scroll progress to a transform — the scroll-linked part.
  const y = useTransform(scrollYProgress, [0, 1], reduce ? [0, 0] : [80, -80]);

  return (
    <section ref={ref}>
      {/* Scroll-triggered reveal: runs once when in view, below the fold only. */}
      <motion.h2
        initial={{ opacity: 0, y: 24 }}
        whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true, margin: '-15%' }}
        transition={{ duration: 0.6, ease: 'easeOut' }}
      >
        Engineered for scroll
      </motion.h2>

      {/* Scroll-linked parallax layer. */}
      <motion.div style={{ y }} />
    </section>
  );
}

Performance and Core Web Vitals: the LCP trap

This is the section most tutorials skip, and it is where scroll animation quietly destroys rankings. This very site once shipped a hero whose main heading started at opacity: 0 and faded in on load. Visually it looked premium. Mechanically it was a disaster: the Largest Contentful Paint element was invisible until the animation finished, so the browser recorded LCP hundreds of milliseconds later than the pixels were actually ready.

Google's Core Web Vitals thresholds, measured at the 75th percentile of real users, are unforgiving here:

  • LCP (Largest Contentful Paint) — good is under 2.5 seconds; over 4.0s is poor.
  • INP (Interaction to Next Paint) — good is under 200ms; it fully replaced FID in March 2024.
  • CLS (Cumulative Layout Shift) — good is under 0.1; scroll reveals that push content inflate it fast.

Never animate the LCP element

The largest above-the-fold text or image is almost always your LCP candidate. Render it statically, on the server, at full opacity. Do not fade it, slide it, or gate it behind JavaScript hydration. On this page the <h1> is a plain, un-animated Server Component element for exactly this reason — it paints instantly and locks in a fast LCP.

Use whileInView for below-the-fold only

Entrance animations are safe once they are out of the initial viewport, because the LCP is already recorded by the time the user scrolls to them. Reserve whileInView, once: true reveals, and scrub effects for content the user has not seen yet. Keep the first screen static and heavy motion deferred, and you get the visual polish without the vitals penalty.

Two more guardrails: animate only transform and opacity so the compositor handles motion off the main thread and CLS stays near zero; and always reserve final dimensions so a reveal never reflows its neighbors.

Accessibility: prefers-reduced-motion

Large parallax, pinned scrubbing, and fast slide-ins can trigger real vestibular discomfort: nausea, dizziness, and disorientation. Respecting the operating-system prefers-reduced-motion setting is both an accessibility requirement and a trust signal. Framer Motion exposes the useReducedMotion hook; GSAP pairs cleanly with gsap.matchMedia() or a plain window.matchMedia check. A global CSS fallback that shortens animation and transition durations under the reduced-motion media query is a sensible backstop.

The correct behavior is not to remove all feedback — it is to jump to the final state instantly instead of traveling through a long, sweeping motion. Users still get the layout; they just skip the journey. Combined with visible focus states and keyboard operability, this keeps a heavily animated page usable for everyone.

Frequently asked questions

Is GSAP free to use in commercial projects?

Yes. After Webflow acquired GreenSock in 2024, GSAP became 100% free for everyone, including all previously paid Club plugins such as ScrollTrigger, ScrollSmoother, SplitText, MorphSVG, DrawSVG, and Inertia. The standard license now covers commercial and client work at no cost, which removes the biggest historical objection to choosing GSAP.

Which is better for React and Next.js, GSAP or Framer Motion?

Framer Motion (now published as the "motion" package) is React-native: it ships declarative components, the whileInView prop, and hooks like useScroll and useTransform that fit the component model with minimal glue. GSAP is framework-agnostic and imperative; in React you run it inside useEffect or the official useGSAP hook. For most component-level UI, Framer Motion is faster to ship. For complex, tightly sequenced timelines and pinned scroll sequences, GSAP ScrollTrigger is more capable.

Does scroll animation hurt SEO or Core Web Vitals?

It only hurts if misused. The most common mistake is starting your hero or main heading at opacity: 0 and fading it in — that delays the Largest Contentful Paint (LCP), whose good threshold is under 2.5 seconds. Keep the LCP element statically rendered and un-animated, animate only below-the-fold content, and use transform and opacity so you avoid layout shift. Done correctly, scroll animation is CWV-neutral.

What is the difference between scroll-triggered and scroll-linked animation?

Scroll-triggered animation fires once when an element enters the viewport — for example a card that fades up as you reach it (Framer Motion whileInView, or GSAP ScrollTrigger without scrub). Scroll-linked animation ties animation progress directly to scroll position, so scrolling scrubs the timeline forward and backward (GSAP scrub, or Framer Motion useScroll mapped through useTransform).

How do I stop scroll animations from causing layout shift (CLS)?

Animate only transform and opacity, never layout-affecting properties like height, top, or margin. Reserve final dimensions up front so nothing reflows, avoid injecting content that pushes the page, and set explicit width and height on media. The good Cumulative Layout Shift threshold is under 0.1 at the 75th percentile of real users.

How do I respect prefers-reduced-motion in GSAP and Framer Motion?

In Framer Motion, use the useReducedMotion hook to skip or shorten transforms. In GSAP, gate animations behind window.matchMedia("(prefers-reduced-motion: reduce)") or gsap.matchMedia(). At minimum, provide the final visual state instantly for users who request reduced motion, which prevents vestibular discomfort from large parallax and scrub effects.

Want motion that passes Core Web Vitals?

I build fast, accessible, cinematic front-ends that animate without sacrificing LCP, INP, or search visibility. Let's engineer yours.

Start a Project