Animating React Components With GreenSock

Animating React Components With GreenSock

During the early days of the World Wide Web, things were rather static and boring. Webpages were mostly based on graphic design and layouts from the print world until animations were introduced. Animation can engage and hold people’s attention longer than a static web page and communicates an idea or concept more clearly and effectively.

However, when not done right, animations can hamper user interactions with your product and negatively impact traction. The GreenSock Animation Platform AKA (GSAP) is a powerful JavaScript library that enables front-end developers, animators and designers to create performant timeline based animations. It allows animation lovers take precise control of their animation sequences rather than the sometimes constraining keyframe and animation properties that CSS offers.

In this article, I’ll introduce you to some features of GSAP such as scrollTriggers, Timelines, Easing etc, at the end we’ll build an intuitive user interface by animating a React app with this features👌. Check out the finished project on codesandbox.

This article will be useful to you if:

  • You have been building animations on web applications with HTML, CSS, and JavaScript.
  • You are already building animated webpages in a React apps with packages like animate.css, React-motion, Framer-motion, and React-Spring, plus you want to check out alternatives.
  • You are a React enthusiast, and you’d like to build complex animations on React-based web applications.

We will look at how to build a variety of animations from an existing web project. Let’s get to it!

Note: This article assumes you are comfortable with HTML, CSS, JavaScript, and React.js.

What Is GSAP?

GreenSock Animation Platform also known as GSAP is an Ultra high-performance, professional-grade animation for the modern web that allows developers to animate their apps in a modular, declarative, and re-usable fashion. It is framework-agnostic and can be used across any JavaScript based project, it has a very minimal bundle size and will not bloat your app.

GSAP can perform canvas animations, used to create WebGL experiences, and create dynamic SVG animations and as great browser support.

Why Use GSAP?

Maybe you’re not quite ready to betray other frameworks yet, or you haven’t been convinced to embrace the goodies that come with GSAP. Allow me to give you a few reason why you may want to consider GSAP.

You Can Build Complex Animations

GSAP JavaScript library makes it possible for developers to build simple to very complex physics-based animations like in the case of these sites, it allows developers and designers sequence motion and controls the animation dynamically. It has lots of plugins such as DrawSVGPlugin, MorphSVGPlugin, and more, which makes creating SVG based animations and 2D/3D animations a reality. Asides integrating GSAP on DOM elements, you can use them within WebGL/Canvas/ Three.js context-based animations.

Furthermore, the easing capacity of GSAP is quite sophisticated, hence making it possible to create advance effects with multiple beziers as compared to the regular CSS animation.

Performance

GSAP has an impressive high performance across different browsers.

According to GSAP’s team, in their website, “GSAP is 20x faster than jQuery, plus GSAP is the fastest full-featured scripted animation tool on the planet. It's even faster than CSS3 animations and transitions in many cases.” Confirm speed comparison for yourself.

Furthermore, the GSAP animations perform effortlessly on both desktop computers, tablets, and smartphones. It is not needed to add a long list of prefixes, this is all being taken care of under the hood by GSAP.

You can check out more benefits on GSAP or see what Sarah Drasner as to say about it here.

Cons Of GSAP

Are you saying I should always use GSAP for every project? Of course not! I feel like, there’s only one reason you might not want to use GSAP. Let’s find out!

  • GSAP is solely a JavaScript-based animation library, hence it requires some knowledge of JavaScript and DOM manipulation to effectively utilize its methods and APIs. This learning curve downside leaves even more room for complications for a beginner starting out with JavaScript.
  • GSAP doesn’t cater to CSS based animations, hence if you are looking for a library for such, you might as well use keyframes in CSS animation.

If you’ve got any other reason, feel free to share it in the comment section.

Alright, now that your doubts are cleared, let’s jump over to some nitty-gritty in GSAP.

GSAP Basics

Before we create our animation using React, let’s get familiar with some methods and building blocks of GSAP.

If you already know the fundamentals of GSAP, you can skip this section and jump straight to the project section, where we’ll make a landing page skew while scrolling.

Tween

A tween is a single movement in an animation. In GSAP, a tween has the following syntax:

TweenMax.method(element, duration, vars)

Let’s take a look at what this syntax represents;

  1. method refers to the GSAP method you’ll like to tween with.
  2. element is the element you want to animate. If you want to create tweens for multiple elements at the same time, you can pass in an array of elements to element.
  3. duration is the duration of your tween. It is an integer in seconds (without the s suffix!).
  4. vars is an object of the properties you want to animate. More on this later.

GSAP methods

GSAP provides numerous methods to create animations. In this article, we’d mention only a few such as gsap.to, gsap.from, gsap.fromTo. You can check out other cool methods in their documentation. The methods discussed in this section will be used in building our project later in this tutorial.

  • gsap.to() the values to which an object should be animated i.e the end property values of an animated object — as shown below:
    gsap.to('.ball', {x:250, duration: 5})

To demonstrate the to method the codepen demo below shows that an element with a class of ball 250px will move across the x-axis in five seconds when the components mounts. If a duration isn't given, a default of 500 milliseconds would be used.

See the Pen GSAP REACT DEMO1 by Blessing Krofegha.

Note: x and y-axis represent the horizontal and vertical axis respectively, also in CSS transform properties such as translateX and translateY they are represented as x and y for pixel-measured transforms and xPercent and yPercent for percentage-based transforms.

To view the complete snippet of the code check the codepen playground.

  • gsap.from() — Defines the values an object should be animated from — i.e., the start values of an animation:
    gsap.from('.square', {duration:3, scale: 4})

The codepen demo show how an element with a class of square is resized from a scale of 4 in 3seconds when the components mounts. Check for the complete code snippet on this codepen.

See the Pen GSAP REACT DEMO2 by Blessing Krofegha.

  • gsap.fromTo() — lets you define the starting and ending values for an animation. It is a combination of both the from() and to() method.

Here’s how it looks;

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 code would animates the element with a class of ball from an opacity of 0 to an opacity of 1 across the x-axis in 3 seconds and the square class is animated the from an opacity of 0 to 1 in 3 seconds across the x-axis only when the component mounts. To see how the fromTo method works and the complete code snippet, check the demo on CodePen below.

See the Pen React GSAP FromTo demo by Blessing Krofegha.

Note: Whenever we’re animating positional properties, such as left and top, we must ensure that the elements concerned must have a CSS position property of either relative, absolute, or fixed.

Easing

GSAP official documentation defined easing as the primary way to change the timing of your Tweens. It determines how an object changes position at different points. Ease controls the rate of change of animation in GSAP and is used to set the style of an object’s animation.

GSAP provides different types of eases and options to give you more control over how your animation should behave. It also provides an Ease Visualizer to help you choose your preferred ease settings.

There are three types of eases, and they vary in their operations.

  1. in() — Motion starts slowly, then picks up the pace toward the end of the animation.
  2. out() — The animation starts fast then slows down at the end of the animation.
  3. inOut() — The animation begins slow, picks up the pace halfway through, and ends slowly.

See the Pen React GSAP Easing demo by Blessing Krofegha.

In these easing example, we chained the tweens that displayed the three types of eases bounce.in, bounce.out and bounce.inOut, and set a delay of the number of seconds it takes the animation to complete before starting the next one only when the component is mounts. This pattern is repetitive, in the next next section we would see how we could use a timeline to do this better.

Timelines

A Timeline acts as a container for multiple tweens. It animates tweens in sequential order, and it is not dependent on the duration of the previous tween. Timeline makes it simple to control tweens as a whole and precisely manage their timing.

Timelines can be written by creating an instance of a timeline like so:

gsap.timeline();

You can also chain multiple tweens to a timeline in two different ways, in the code below:

##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', {})

Let’s recreate the previous 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, we created a variable(tl) that holds an instance of a timeline, next we used the tl variable to animate our tween in sequential without depending on the previous tween to animate, passing the same properties as it were in the previous example. For the complete code snippet of this demo check the codepen playground below.

See the Pen React GSAP (Easing with Timeline) demo by Blessing Krofegha.

Now that we have gotten a feel of some the basic building blocks of GSAP, let’s see how we could build a complete animation in a typical React app in the next section. Let’s begin the flight! 🚀

Building an Animated Landing Page with React and GSAP

Let’s get to animate a React App. Ensure you clone the repo before you begin and run npm install, to install the dependencies.

What are we building?

Currently, our landing page contains a few texts a white background, a menu that doesn’t drop down, with really no animation. The following are what we’ll be adding to the landing page;

  • Animate the text and the logo on the homepage, so it eases out when the component is mounted.
  • Animate the menu, so it drops down when the menu is clicked.
  • Make the images in the gallery page skew 20deg when the page scrolls.
Animated page.

Check out the demo on codesandbox.

We’ll break the process of our landing page into components, so it will be easy to grasp. Here’s the process;

  • Define the animation methods,
  • Animate text and logo,
  • Toggle menu,
  • Make images skew 20deg on page scroll.

components

  • Animate.js — Defined all animation methods,
  • Image.js — import galley images,
  • Menu.js — Contains the menu toggle functionality,
  • Header.js — Contains navigation links.

Define animation methods

Create a component folder inside the src directory, and create an animate.js file. Copy and paste the following code into it.

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",
  });
};

Here, we imported gsap . We wrote an exported arrow function that animates the text on the landing page. Remember that gsap.from() method defines the values an object should be animated from. The function has an elem parameter that represents the class which needs to be animated. It takes a few properties and assigns values such as xPercent: -20 (transforms the object by -20%), gives the object no opacity, makes the object scale by -1, makes the object ease back in 2sec.

To see if this works, head over to App.js and include the following code.

...
//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>
  );
}

Here, we import the textIntro method from the Aminate component. To access the DOM we used to useRef Hook. We created a variable intro whose value is set to null. Next, inside the useEffect hook, we called the textIntro method and the intro variable. Inside our home component, in the h5 tag, we defined the ref prop and passed in the intro variable.

Animated text.

Next, we have got a menu, but it isn’t dropping down when it’s clicked. Let’s make it work! Inside the Header.js Component, add the code below.

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);

In this component, we defined our menu and button state, inside the useEffect hook, we listened for page changes using useHistory hook, if the page changes we set the clicked and menuName state values to false and Menu respectively.

To handle our menu, we checked if the value of our initial state is false, if true, we change the value of initial , clicked and menuName to null, true and Close. Else we check if the button is clicked, if true we’d change the menuName to Menu. Next, we have a disabledMenu function that disables our button for 1sec when it’s clicked.

Lastly, in our button, we assigned disabled to disabled which is a boolean value that will disable the button when its value is true. And the onClick handler of the button is tied to the toggleMenu function. All we did here was toggle our menu text and passed the state to a Menu component, which we would create soonest. Let’s write the methods that will make our menu dropdown before creating the actual Menu component. Head over to Animate.js and paste this code into it.

....
//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,
    },
  });
};

Here, we have a function called menuShow, which skews the menu horizontally by 2degrees, eases the menu, offset’s the animation using the stagger property, and transforms the menu from right to top in 0.7sec, the same properties go for the menuHide function. To use these functions, create Menu.js file inside the components and paste this code into it.

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

What we did in the Menu component was to import the animated functions, which are menuShow, menuHide, and textIntro. Next, we assigned variables for each created refs for our DOM elements using the useRef hook and passed null as their values. Inside the useEffect hook, we check for the state of the menu, if clicked is false, we call the menuHide function, otherwise, if the clicked state is true we call the menuShow function. Lastly, we ensured that the DOM elements concerned are passed their specific refs which are menuWrapper, show1, show2. With that, we’ve got our menu animated.

Let’s see how it looks.

Animated Menu.

The last animation we would implement is make our images in our gallery skew when it scrolls. Let’s see the state of our gallery now.

Gallery without animation.

To implement the skew animation on our gallery, let’s head over to Animate.js and add a few codes to it.

....
//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,
        });
      },
    });
}

We created a function called skewGallery, passed elem1 as a param, and registered ScrollTrigger.

ScrollTrigger is a plugin in GSAP that enables us to trigger scroll-based animations, like in this case of skewing the images while the page scrolls.

To make the right edge stick to the scroll bar we passed right center value to the transformOrigin property, we also set the force3D property to true in other to improve the performance.

We declared a clamp variable that calculates our skew and ensures it doesn’t exceed 20degs. Inside the ScrollTrigger object, we assigned the trigger property to the elem1 param, which would be the element that needs to be triggered when we call this function. We have an onUpdate callback function, inside it is a velocity variable that calculates the current velocity and divides it by 300.

Lastly, we animate the element from their current values by setting other values. We set skew to initially be at 0 and skewY to be the velocity variable at 0.8.

Next, we’ve got to call this function in our App.js file.

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

....

Here, we imported skewGalley from ./components/Animate, created a skewImage ref that targets the image element. Inside the useEffect hook, we called the skewGallery function and passed the skewImage ref as a param. Lastly, we passed the skewImage to the ref to attribute.

You’d agree with me it was such a pretty cool journey thus far. Here’s the preview on CodeSanbox 👇

The supporting repo for this article is available on Github.

Conclusion

We’ve explored the potency of GSAP in a React project, we only scratched the surface in this article, there’s no limit to what you can do with GSAP as it concerns animation. GSAP’s official website offers additional tips to help you gain a thorough understanding of methods and plugins. There’s a lot of demos that would blow your mind away with what people have done with GSAP. I’d love to hear your experience with GSAP in the comment section.

Resources

  1. GSAP Documentation — GSAP
  2. The Beginner’s Guide to the GreenSock Animation Platform — Freecodecamp
  3. An introduction to animations with Greensock Animation API (GSAP) — Zellwk