What Makes GSAP Different

The GreenSock Animation Platform (GSAP) is a JavaScript library for tweening values, attributes, or CSS properties over time. Unlike CSS keyframe and animation properties, which can feel constraining for complex sequences, GSAP gives developers fine-grained control through timelines and a plugin ecosystem. It works across any JavaScript project, supports canvas, WebGL, and SVG animations, and has broad browser support.

GSAP’s main selling point is its ability to handle intricate, physics-based motion. Plugins like DrawSVGPlugin and MorphSVGPlugin make advanced SVG work possible, and the library integrates with Three.js contexts as well.

Performance and Limitations

GSAP's documentation claims it is 20x faster than jQuery and often faster than CSS3 animations and transitions. Animations run smoothly across desktops, tablets, and smartphones without requiring long vendor-prefix lists.

There are, however, trade-offs. GSAP is purely JavaScript-based, so it demands a working knowledge of JavaScript and DOM manipulation, which steepens the learning curve for beginners. It also doesn't handle CSS-based animations, so for those cases you'd turn to native CSS keyframes.

Core Building Blocks

Before diving into a React implementation, it helps to understand the fundamental pieces GSAP offers.

Tweens

A tween animates a single property from one value to another over a given duration. GSAP provides methods like gsap.to(), gsap.from(), and gsap.fromTo() to define these transitions, with options for delay, ease, and repeat.

Timelines

Timelines let you sequence multiple tweens so they play in order or overlap precisely. You can position animations relative to each other, which makes orchestrating complex scenes much easier than coordinating separate tweens manually.

Easing

GSAP's easing functions go beyond the standard CSS timing functions, enabling multi-bezier curves and advanced effects through its easing visualizer.

ScrollTrigger

The ScrollTrigger plugin ties animation to scroll position. You can trigger tweens or timelines when elements enter or leave the viewport, enabling scroll-driven storytelling on a landing page.

GSAP Building Blocks

Before diving into the React project, it helps to understand the core concepts behind GSAP animations: tweens, methods, easing, and timelines.

Tweens

A tween represents a single animation movement. The basic syntax looks like this:

TweenMax.method(element, duration, vars)

Breaking that down:

  1. method — the GSAP method used to create the tween.
  2. element — the target element (or an array of elements) to animate.
  3. duration — the animation length in seconds, written without the s suffix.
  4. vars — an object containing the CSS properties to animate.

Core Methods

GSAP offers several tween methods. The three used most often are:

  • gsap.to() — animates an element to the specified end values:
gsap.to('.ball', {x:250, duration: 5})

This moves an element with the class ball 250px along the x-axis over five seconds when the component mounts. If no duration is set, GSAP defaults to 500 milliseconds.

  • gsap.from() — defines the starting values of an animation:
gsap.from('.square', {duration:3, scale: 4})

This example scales an element with the class square from a scale of 4 down to its natural size over three seconds on mount.

  • gsap.fromTo() — combines from() and to() to set both the start and end values:
gsap.fromTo('.ball',{opacity:0 }, {opacity: 1 , x: 200 , duration: 3 });
gsap.fromTo('.square', {opacity:0, x:200}, { opacity:1, x: 1 , duration: 3 });

This animates the ball class from opacity 0 to 1 across the x-axis, and the square class the same way, over three seconds on mount.

Note: when animating positional properties like left or top, the target element must have a CSS position of relative, absolute, or fixed.

Easing

Easing controls the rate of change over the course of an animation. GSAP's official Ease Visualizer helps you preview available options. There are three main ease types:

  1. in() — starts slow, accelerates toward the end.
  2. out() — starts fast, decelerates at the end.
  3. inOut() — slow at both ends, fast in the middle.

Timelines

A timeline acts as a container for multiple tweens, sequencing them so each one plays after another without hard-coding delays. Create an instance like this:

gsap.timeline();

You can chain tweens in two ways:

##Method 1
const tl = gsap.timeline(); // create an instance and assign it a variable
tl.add(); // add tween to timeline 
tl.to('element', {});
tl.from('element', {});

##Method 2
gsap.timeline()
    .add() // add tween to timeline 
    .to('element', {})
    .from('element', {})

Recreating the easing example with a timeline:

const { useRef, useEffect } = React;

const Balls = () => {
    useEffect(() => {      
    const tl = gsap.timeline();
    tl.to('#ball1', {x:1000, ease:"bounce.in", duration: 3})
    tl.to('#ball2', {x:1000, ease:"bounce.out", duration: 3, delay:3 })
    tl.to('#ball3', {x:1000, ease:"bounce.inOut", duration: 3, delay:6 })
  }, []);
}

ReactDOM.render(, document.getElementById('app'));

Inside a useEffect hook, the tl variable holds the timeline instance and sequences the tweens without depending on the previous tween's duration.

Building An Animated Landing Page

Now let's apply these concepts in a React app. Clone the supporting repo and run npm install first.

The starter project is a basic landing page with text, a white background, and a menu that doesn't yet drop down. The goal is to:

  • Animate the text and logo to ease in on mount.
  • Make the menu drop down when clicked.
  • Skew the gallery images 20 degrees on page scroll.

The implementation is split across components:

  • Animate.js — defines all animation methods.
  • Image.js — imports gallery images.
  • Menu.js — handles menu toggle functionality.
  • Header.js — contains navigation links.

Text and Logo Animation

Start by creating a component folder inside src, then add an animate.js file with this code:

import gsap from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger";
//Animate text 
export const textIntro = elem => {
  gsap.from(elem, {
    xPercent: -20,
    opacity: 0,
    stagger: 0.2,
    duration: 2,
    scale: -1,
    ease: "back",
  });
};

This exports a textIntro arrow function that takes an elem parameter (the class to animate). It uses gsap.from() to set the starting state: xPercent: -20 shifts it left by 20%, opacity is zero, and the element scales by -1, with a back ease over 2 seconds.

To wire it up, update App.js:

...
//import textIntro
import {textIntro} from "./components/Animate"

...
//using useRef hook to access the textIntro DOM
 let intro = useRef(null)
  useEffect(() => {
    textIntro(intro)
  }, [])

function Home() {
  return (
    <div className='container'>
      <div className='wrapper'>
        <h5 className="intro" ref={(el) => (intro = el)}></h5>
          The <b>SHOPPER</b>, is a worldclass, innovative, global online ecommerce platform,
          that meets your everyday daily needs.
        </h5>
      </div>
    </div>
  );
}

Import textIntro, use useRef to access the DOM, and call the animation inside useEffect with the ref assigned to the h5 element.

The menu exists but doesn't respond to clicks. Add toggle logic to Header.js:

import React, { useState, useEffect, useRef } from "react";
import { withRouter, Link, useHistory } from "react-router-dom";
import Menu from "./Menu";
const Header = () => {
  const history = useHistory()
  let logo = useRef(null);
  //State of our Menu
  const [state, setState] = useState({
    initial: false,
    clicked: null,
    menuName: "Menu",
  });
  // State of our button
  const [disabled, setDisabled] = useState(false);
  //When the component mounts
  useEffect(() => {
    textIntro(logo);
    //Listening for page changes.
    history.listen(() => {
      setState({ clicked: false, menuName: "Menu" });
    });
  }, [history]);
  //toggle menu
  const toggleMenu = () => {
    disableMenu();
    if (state.initial === false) {
      setState({
        initial: null,
        clicked: true,
        menuName: "Close",
      });
    } else if (state.clicked === true) {
      setState({
        clicked: !state.clicked,
        menuName: "Menu",
      });
    } else if (state.clicked === false) {
      setState({
        clicked: !state.clicked,
        menuName: "Close",
      });
    }
  };
  // check if out button is disabled
  const disableMenu = () => {
    setDisabled(!disabled);
    setTimeout(() => {
      setDisabled(false);
    }, 1200);
  };
  return (
    <header>
      <div className="container">
        <div className="wrapper">
          <div className="inner-header">
            <div className="logo" ref={(el) => (logo = el)}>
              <Link to="/">SHOPPER.</Link>
            </div>
            <div className="menu">
              <button disabled={disabled} onClick={toggleMenu}>
                {state.menuName}
              </button>
            </div>
          </div>
        </div>
      </div>
      <Menu state={state} />
    </header>
  );
};
export default withRouter(Header);

This component tracks menu state with useState. A useEffect hook listens to route changes via useHistory, resetting the menu to its closed state on navigation. The toggleMenu handler flips the state between open and closed, updating the button text between "Menu" and "Close." The disabledMenu function disables the button for one second after each click.

Next, add the animation methods back in Animate.js:

....
//Open menu
export const menuShow = (elem1, elem2) => {
  gsap.from([elem1, elem2], {
    duration: 0.7,
    height: 0,
    transformOrigin: "right top",
    skewY: 2,
    ease: "power4.inOut",
    stagger: {
      amount: 0.2,
    },
  });
};
//Close menu
export const menuHide = (elem1, elem2) => {
  gsap.to([elem1, elem2], {
    duration: 0.8,
    height: 0,
    ease: "power4.inOut",
    stagger: {
      amount: 0.07,
    },
  });
};

The menuShow and menuHide functions both skew the menu by 2 degrees horizontally, apply an ease, use a stagger offset, and translate the menu from right to top over 0.7 seconds.

Now create the Menu.js component:

import React, {useEffect, useRef} from 'react'
import { gsap } from "gsap"
import { Link } from "react-router-dom"
import {
  menuShow,
  menuHide,
  textIntro,
} from './Animate'
const Menu = ({ state }) => {
   //create refs for our DOM elements
  
  let menuWrapper = useRef(null)
  let show1 = useRef(null)
  let show2 = useRef(null)
  let info = useRef(null)
  useEffect(() => {
    // If the menu is open and we click the menu button to close it.
    if (state.clicked === false) {
      // If menu is closed and we want to open it.
      menuHide(show2, show1);
      // Set menu to display none
      gsap.to(menuWrapper, { duration: 1, css: { display: "none" } });
    } else if (
      state.clicked === true ||
      (state.clicked === true && state.initial === null)
    ) {
      // Set menu to display block
      gsap.to(menuWrapper, { duration: 0, css: { display: "block" } });
      //Allow menu to have height of 100%
      gsap.to([show1, show2], {
        duration: 0,
        opacity: 1,
        height: "100%"
      });
      menuShow(show1, show2);
      textIntro(info);
      
    }
  }, [state])
  
  return (
    <div ref={(el) => (menuWrapper = el)} className="hamburger-menu">
      <div
        ref={(el) => (show1 = el)}
        className="menu-secondary-background-color"
      ></div>
      <div ref={(el) => (show2 = el)} className="menu-layer">
        <div className="container">
          <div className="wrapper">
            <div className="menu-links">
              <nav>
                <ul>
                  <li>
                    <Link
                      ref={(el) => (line1 = el)}
                      to="/about-us"
                    >
                      About
                    </Link>
                  </li>
                  <li>
                    <Link
                      ref={(el) => (line2 = el)}
                      to="/gallery"
                    >
                      Gallery
                    </Link>
                  </li>
                  <li>
                    <Link
                      ref={(el) => (line3 = el)}
                      to="/contact-us"
                    >
                      Contact us
                    </Link>
                  </li>
                  
                </ul>
              </nav>
              <div ref={(el) => (info = el)} className="info">
                <h3>Our Vision</h3>
                <p>
                  Lorem ipsum dolor sit amet consectetur adipisicing elit....
                </p>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
export default Menu

This imports menuShow, menuHide, and textIntro. It sets up refs for the DOM elements and checks the clicked state inside useEffect, calling either menuHide (when closed) or menuShow (when open). Each animated element gets its specific ref assigned.

Skew on Scroll

The final effect makes gallery images skew as the page scrolls. Add this to Animate.js:

....
//Skew gallery Images
export const skewGallery = elem1 => {
  //register ScrollTrigger
  gsap.registerPlugin(ScrollTrigger);
  // make the right edge "stick" to the scroll bar. force3D: true improves performance
    gsap.set(elem1, { transformOrigin: "right center", force3D: true });
    let clamp = gsap.utils.clamp(-20, 20) // don't let the skew go beyond 20 degrees. 
    ScrollTrigger.create({
      trigger: elem1,
      onUpdate: (self) => {
        const velocity = clamp(Math.round(self.getVelocity() / 300));
        gsap.to(elem1, {
          skew: 0,
          skewY: velocity,
          ease: "power3",
          duration: 0.8,
        });
      },
    });
}

The skewGallery function registers GSAP's ScrollTrigger plugin, which enables scroll-based animations. Setting transformOrigin to right center keeps the right edge anchored to the scroll bar. The force3D property is enabled for performance.

A clamp variable caps the skew at 20 degrees. Inside the ScrollTrigger object, trigger points to the gallery element. An onUpdate callback calculates velocity by dividing the current scroll velocity by 300, then animates skew and skewY based on that value.

Finally, call this function in App.js:

....
import { skewGallery } from "./components/Animate"
function Gallery() {
  let skewImage = useRef(null);
  useEffect(() => {
    skewGallery(skewImage)
  }, []);
  return (
    <div ref={(el) => (skewImage = el)}>
      <Image/>
    </div>
  )
}

....

Import skewGallery, create a skewImage ref targeting the image element, invoke the function inside useEffect with the ref as an argument, and pass the ref to the element.

The complete, working demo is available on CodeSandbox, and the source code lives in the GitHub repo.

Where GSAP Shines in React

The examples in this walkthrough only begin to show what GSAP can do inside a React application. The library's real power shows up when you move beyond simple fades and slides: scrub-linked scroll animations, staggered timeline sequences, and physics-based motion are all straightforward to express with GSAP's API once the component lifecycle is handled correctly.

The key pattern that makes GSAP and React coexist cleanly is delegation: let React own the DOM structure and state, while GSAP owns the animation timeline. By creating animations inside useEffect and cleaning them up with the returned function, you avoid the classic pitfalls of stale closures and duplicated tweens. For repeat animations or scroll-driven effects, wrapping your logic in useGSAP and relying on its dependency array keeps everything synchronized with React's render cycle.

If you are animating something that exists only while a component is mounted — like a modal or a tooltip — make the enter animation part of the mount effect and the exit animation part of the cleanup. In cases where you need to animate between two different states of the same element, a timeline with yoyo: true or a simple progress() update gives you precise control without mounting or unmounting anything.

Going Further

The features covered here — gsap.to(), ScrollTrigger, and staggered reveals — represent a fraction of the toolkit. The official GSAP site documents the position parameter and every plugin in depth, with demos that showcase production-grade animation patterns. For a broader grounding, the GSAP documentation is the definitive reference, while introductory guides from freeCodeCamp and Zell Liew offer practical starting points for developers new to the animation platform.

Smashing Editorial

(Topics: frontend, React, animation)