Skip to content

GSAP & Motion

Using GSAP in React and Next.js without fighting the framework

Practical patterns for GSAP animation in React and the Next.js App Router: scoping, cleanup, Strict Mode, reduced motion and content that never stays hidden.

By Aakash Nishad, Founder & Developer

3 min read

GSAP is one of the most capable animation libraries on the web, and React is one of the most popular ways to build interfaces. Used together carelessly, though, they step on each other: animations run twice, elements get stuck invisible, and ScrollTriggers pile up between page navigations. These are the patterns we use to keep the two working together.

1. Let useGSAP handle the lifecycle

The official @gsap/react package provides a useGSAP hook. It behaves like an effect, but every animation and ScrollTrigger created inside it is recorded in a GSAP context and reverted automatically when the component unmounts. That one detail removes most of the cleanup bugs people run into.

"use client";

import { useRef } from "react";
import { gsap } from "gsap";
import { useGSAP } from "@gsap/react";

gsap.registerPlugin(useGSAP);

export function Reveal({ children }: { children: React.ReactNode }) {
  const scope = useRef<HTMLDivElement>(null);

  useGSAP(
    () => {
      gsap.from(".item", { y: 32, autoAlpha: 0, stagger: 0.08 });
    },
    { scope }, // selectors only match inside this element
  );

  return <div ref={scope}>{children}</div>;
}

The scope option matters as much as the cleanup. Without it, a selector like .item matches every element on the page with that class — including ones owned by other components.

2. Expect Strict Mode to run your code twice

In development, React Strict Mode mounts, unmounts and remounts components to surface side-effect bugs. An animation written with a bare useEffect and no cleanup will start twice, and gsap.from() will often capture the half-animated state as its starting point. Because useGSAP reverts on unmount, the second run starts from a clean slate.

3. Never let content depend on JavaScript to appear

A common pattern is to hide elements with CSS (opacity: 0) and then animate them in. If the script fails to load, or a visitor has reduced motion enabled, that content is simply gone. A safer approach is to opt in to the hidden state only when animation is actually going to run.

  • Run a tiny inline script before first paint that adds a class such as motion-ok to the html element — but only when prefers-reduced-motion is not set.
  • Scope your initial hidden styles to that class: html.motion-ok [data-reveal] { opacity: 0 }.
  • Add a fallback timer that removes the class if the animation runtime hasn't started after a few seconds.

4. Watch out for transforms set in CSS

GSAP reads an element's existing transform and parses it into its own x and y values. If your CSS pre-hides a line with transform: translateY(110%) and you then animate yPercent back to 0, GSAP may preserve the parsed pixel offset — and the text stays hidden. Set both explicitly in a fromTo tween so the result doesn't depend on what was parsed.

gsap.fromTo(
  "[data-line] > span",
  { yPercent: 110, y: 0 },
  { yPercent: 0, y: 0, duration: 1.2, stagger: 0.1, ease: "expo.out" },
);

5. Refresh ScrollTrigger when layout settles

ScrollTrigger calculates start and end positions when it's created. Web fonts that load late, images without dimensions, and content that expands after hydration all shift the layout and make those positions stale. Give images explicit sizes, and call ScrollTrigger.refresh() once fonts are ready.

document.fonts?.ready.then(() => ScrollTrigger.refresh());

6. Animate the cheap properties

Transforms and opacity can be handled by the compositor without recalculating layout. Animating width, height, top or margin forces layout work on every frame, which is exactly what stutters on a mid-range phone. If an effect seems to need a layout property, there is usually a transform-based way to get the same look.

Summary

  • Use useGSAP with a scope for automatic, contained cleanup.
  • Assume effects run twice in development.
  • Only hide content when animation will definitely run.
  • Use explicit fromTo values when CSS also sets transforms.
  • Refresh ScrollTrigger after fonts and late content settle.
  • Prefer transform and opacity for smooth motion.
Written by Aakash Nishad. Published .
All insights
  • 2 min read

    What to prepare before starting a website project

    A practical checklist for business owners: the information, content and decisions that make a website project faster, clearer and easier to estimate.

    By Aakash Nishad

  • 2 min read

    Core Web Vitals, explained for business owners

    What LCP, INP and CLS measure, why they matter for real visitors, and the most common reasons small business websites score poorly.

    By Aakash Nishad

Want this kind of care
in your project?

If you're planning a website or app and want it built thoughtfully, let's talk.