GSAP in React - Part 2: Timelines

Andrew Umstead
August 4th, 2020

*Note: You may want to browse part 1 of this guide if you want to learn more about the basics of GSAP, tweens, and how I use them in React with the useEffect and useRef hooks.

Timelines make sequenced or complex animations much easier. It's possible to set a delay for each individual tween, similar to what you might do in a sequenced CSS animation. You might think, like I did, that if you already know how to write simple animations, then maybe it's easier to just use delay to stagger them so that they run smoothly.

Well, I tried that for the animation we're making here, and it got ugly rather quickly.

Hover or tap to trigger animation.

With the icons sequence comprising of more than 30 tweens, any change in duration or delay resulted in a change for every tween thereafter.

This is where the beauty of GSAP timelines comes in, as every individual tween takes a parameter that gives you a bunch of options about the tween's timing, rather than having to always delay its runtime from the start. The position parameter, as it's called, gives you an enormous amount of control over when a tween animates.

The result is that when you change the duration or delay of a tween, all the other tweens will fall in line with the sequence, or timeline, as a whole. So, in the end, when you change an animation's timing, you typically only have to change one or two values.

Creating a Timeline

In part 1 about basic tweens we covered how to take advantage of the useRef hook and the .current property it gives us. In myTween.current we stored our tween instance and had access to it throughout the component's scope.

Conveniently, for a timeline, we will create, store, and play it in almost the exact same way we did for a tween.

We can begin by simply looking at an example animation made with a GSAP timeline. Hover or tap and you'll see three tweens animate sequentially.

To create this simple timeline, we'll first make a component that renders the SVG.

import styles from "./MyTimelineComponent.module.scss";
import { useRef, useEffect } from "react";

function MyTimelineComponent() {
  const myTimeline = useRef(null);

  useEffect(() => {
    console.log('use effect running')
  }, []);

  function handleMouseEnter() {
    console.log('mouse entered')
  }

  return (
    <svg
      className={styles.svg}
      onMouseEnter={handleMouseEnter}
      xmlns="http://www.w3.org/2000/svg"
      ...

All we're doing here is importing some styling and our hooks, and creating the component for the SVG and animation. The first line of non-boiler-plate code is when we use the useRef hook and store its returned object into myTimeline. Then, useEffect will run after the component has mounted, and the handler runs on the mouse enter event. The rest of the SVG code is absent, since it's hundreds of lines.

*Note: This particular SVG can be downloaded for free at illlustrations.co. In Figma, I deleted some of the graphics we won't be using for simplicity's sake.

For the next step, in the useEffect hook, we'll instantiate our timeline and store it in myTimeline.current.

useEffect(() => {
  myTimeline.current = gsap.timeline();
}, [])

Easy enough. Now, we have a timeline stored in myTimeline.current, to which we can attach any number of tweens. We can do this by calling the .to() method, similar to how we would create a tween. The difference is that we're calling .to() on the timeline instance, rather than the gsap object. We already used the gsap object above to instantiate our timeline with the .timeline() method.

By calling .to() on our timeline instance, each tween we create is run sequentially. GSAP understands that they are meant to run one after the other. It also allows us to chain our .to() calls, rather than type out myTimeline.current.to() every time, although you could if you wanted.

useEffect(() => {
  myTimeline.current = gsap.timeline();
  myTimeline.current
    .to("#gear-group", {
      duration: 1,
      rotate: 360,
      transformOrigin: "50% 50%"
    })
    .to("#red-icon-group", { duration: 1, x: 45 })
    .to("#green-icon-group", { duration: 1, y: -48 })
}, [])

Above you can see the creation of 3 tweens — one with each .to() call. The first targets the gear and animates a couple CSS properties to create a rotating effect. The second targets the red icon and moves it to the right on its x-axis. And, the third moves the green icon upwards on its y-axis. Each tween lasts for 1 second and the x and y values were chosen through experimentation.

For a basic timeline, that's pretty much it. We can .pause() it in the useEffect hook before it has a chance to animate, and then .restart() it when our event handler is triggered, thus running the animation.

Here's the finished component.

import styles from "./MyTimelineComponent.module.scss";
import { useRef, useEffect } from "react";

function MyTimelineComponent() {
  const myTimeline = useRef(null);

  useEffect(() => {
    myTimeline.current = gsap.timeline();
    myTimeline.current
      .to("#gear-group", {
        duration: 1,
        rotate: 360,
        transformOrigin: "50% 50%"
      })
      .to("#red-icon-group", { duration: 1, x: 45 })
      .to("#green-icon-group", { duration: 1, y: -48 })

    myTimeline.current.pause();
  }, []);

  function handleMouseEnter() {
    myTimeline.current.restart();
  }

  return (
    <svg
      className={styles.svg}
      onMouseEnter={handleMouseEnter}
      xmlns="http://www.w3.org/2000/svg"
      ...

Using the Positioner Parameter

The positioner parameter is really what makes timelines so useful. Before we get into it, let's add a couple more tweens to our animation.

useEffect(() => {
  myTimeline.current = gsap.timeline();
  myTimeline.current
    .to("#gear-group", {
      duration: 1,
      rotate: 360,
      transformOrigin: "50% 50%"
    })
    .to("#red-icon-group", { duration: 1, x: 45 })
    .to("#green-icon-group", { duration: 1, y: -48 })
    .to("#blue-icon-group4", { duration: 1, y: 48 })
    .to("#yellow-icon-group4", { duration: 1, y: -48 })
}, [])

Now, we have a total of 5 tweens and can play around with their timings.

If these were all individual tweens, everything would animate together as soon as the handler is called, and it would look horrible. But, we have a timeline and each tween waits for the previous one to finish.

So, what if we want to make the first icon move right at the same time the gear starts to rotate?

Well, to control when the #red-icon-group tween begins, we can use the position parameter. I highly recommend visiting the Greensock page on timelines in the docs to get a feel for the position parameter and the values it can take. There aren't terribly many values, but each one has its own specific purpose and use case.

For our small animation, we'll only be using two kinds of values that control a tween's timing in relation to its immediate predecessor. Simply inserting "<" as the position parameter will make a tween start at the same time as the one right before it.

In the last line after the vars object, "<" is inserted as the position parameter.

myTimeline.current
  .to("#gear-group", {
    duration: 1,
    rotate: 360,
    transformOrigin: "50% 50%"
  })
  .to("#red-icon-group", { duration: 1, x: 45 }, "<")

The Greensock docs recommend placing the position parameter after the vars object, as done above.

Now, the red icon starts to move on its x-axis at the same time the gear starts rotating.

Notice, the other icons fall in line with the sequence, and the animation runs smoothly.

However, in our final animation, each icon tween moves much faster at .2 seconds, rather than the 1 second they're currently set at. If we go ahead and change the duration property in the vars object to { duration: .2 } — we get the following result.

What's happening is the other tweens are waiting for the gear tween to finish. If we were to set the gear duration to 5 seconds, we'd have a five second delay before the green icon started animating. Of course, this is because in a timeline everything runs sequentially. GSAP can't know we want our gear to rotate simultaneously with the icon animations running.

To sort it out we have to add position parameters to the other icons.

Let's say we want it to be sequential again, with the exception of our gear. We want the green icon to move immediately after the red, blue after green, and so on.

What we'll do is add "<.2" as the position parameter for the three remaining icon tweens. This will delay the respective tween by .2 seconds in relation to the tween directly before it.

So, the red icon's duration is .2 seconds, and because the green icon has "<.2" set as its position parameter, it will wait .2 seconds from the start of the red icon.

The result is a smooth animation from one tween to the next.

The green, blue, and yellow icon tweens are each waiting .2 seconds from the tween directly before it. The code is probably easier to understand than a written explanation.

myTimeline.current
  .to("#gear-group", {
    duration: 1,
    rotate: 360,
    transformOrigin: "50% 50%"
  })
  .to("#red-icon-group", { duration: .2, x: 45 }, "<")
  .to("#green-icon-group", { duration: .2, y: -48 }, "<.2")
  .to("#blue-icon-group4", { duration: .2, y: 48 }, "<.2")
  .to("#yellow-icon-group4", { duration: .2, y: -48 }, "<.2")
}, [])

What if we always want two icons to move at the same time together? This is what our final animation does. Well, knowing what we know about position parameters, this should be easy enough.

If the red icon tween looks like this:

.to("#red-icon-group", { duration: .2, x: 45 }, "<")

...then we'll want the green icon to animate with it, so it'll take "<" as its position parameter.

.to("#green-icon-group", { duration: .2, y: -48 }, "<")

Then, we'll want the blue icon to wait for .2 seconds before it starts. If we omitted the position parameter, the blue icon would wait for the gear to finish, which is not what we want. Instead, we want to control it relative to its immediate predecessor. So, we'll pass "<.2" to the position parameter.

.to("#blue-icon-group", { duration: .2, y: 48 }, "<.2")

Lastly, we're moving two icons together, so the yellow one will start with the blue. Therefore, it'll take "<".

.to("#yellow-icon-group", { duration: .2, y: -48 }, "<")

And, voilà! Our icons are moving two at a time.

Making the Final Animation

To put together the animation we originally set out to make really only requires a few minor additions and some adjustments to the tweens we've already made. For one, I'll bring back the graphics I removed earlier so I have the original SVG. And, then, to start, we can make the gear animation last for a good amount of time.

To do that, we'll just increase the duration. Mine is set to 8 seconds. Now, 8 seconds to rotate 360 degrees makes the gear move quite slowly, so you'll want to increase the rotate value as well. I just multiplied mine by 4, which ensures a full revolution made by the gear. If you don't use a multiple of 360, the gear will finish in a different spot, and on .restart() the animation will be jerky.

myTimeline.current
.to("#gear-group", {
  duration: 8,
  rotate: 360 * 4,
  transformOrigin: "50% 50%",
  ease: "power3.out",
})

Lastly, with the gear, you may recall the animation has the effect of slowing down as the gear loses steam. This is achieved with the ease property seen in the vars object above.

The ease property does the same thing as the CSS transition-timing-function one. Whereas CSS has built-in bezier curves like linear and ease-in-out, GSAP has its own set of built-in eases. The docs has a great vizualizer here, and you can play around with it and copy/paste the ease you prefer. I used power3.out.

In terms of the icons, you should have everything you need to make the timeline yourself. When choosing the direction for the icons' movement, I just avoided overlapping them, and tried to keep it somewhat balanced on the screen, so not all four were in the bottom-right corner, for example.

However, at a certain point, you'll want to start adjusting the duration of the tweens to have them slow down in line with the ease, if you choose to use one. In my 32 tween timeline, I started my slow-down on the 8th from the end. I ended with my last tween lasting 1 full second. Of course, you'll also need to adjust your position parameters accordingly.

If you'd rather just use the animation I made, you can find my timeline code in the component below. You'll have to add the id's to the SVG yourself, though. I use Figma, and I wrote about how I do it in part 1.

import styles from "./Final.module.scss";
import { useEffect, useRef } from "react";

function Final() {
  const tl = useRef(null);

  useEffect(() => {
    tl.current = gsap.timeline();

    tl.current
      .to("#gear-group", 8, {
        rotate: 360 * 4,
        transformOrigin: "50% 50%",
        ease: "power3.out",
      })
      .to("#blue-icon-group", 0.2, { y: 48 }, "<")
      .to("#yellow-icon-group", 0.2, { y: -48 }, "<")
      .to("#green-icon-group", 0.2, { y: -96 }, "<.2")
      .to("#red-icon-group", 0.2, { y: -48 }, "<")
      .to("#green-icon-group", 0.2, { x: 45 }, "<.2")
      .to("#blue-icon-group", 0.2, { x: -45 }, "<")
      .to("#red-icon-group", 0.2, { x: 45 }, "<.2")
      .to("#yellow-icon-group", 0.2, { x: 45 }, "<")
      .to("#blue-icon-group", 0.2, { y: 0 }, "<.2")
      .to("#green-icon-group", 0.2, { x: 0 }, "<")
      .to("#red-icon-group", 0.2, { y: 0 }, "<.2")
      .to("#yellow-icon-group", 0.2, { y: 0 }, "<.")
      .to("#blue-icon-group", 0.2, { x: 0 }, "<.2")
      .to("#green-icon-group", 0.2, { y: -144 }, "<.")
      .to("#red-icon-group", 0.2, { x: 0 }, "<.2")
      .to("#yellow-icon-group", 0.2, { x: 0 }, "<")
      .to("#green-icon-group", 0.2, { x: 45 }, "<.2")
      .to("#blue-icon-group", 0.2, { y: -48 }, "<")
      .to("#yellow-icon-group", 0.2, { y: -48 }, "<.2")
      .to("#red-icon-group", 0.2, { x: -45 }, "<")
      .to("#blue-icon-group", 0.2, { y: -96 }, "<.2")
      .to("#green-icon-group", 0.2, { y: -96 }, "<")
      .to("#red-icon-group", 0.2, { y: 48 }, "<.2")
      .to("#yellow-icon-group", 0.2, { x: 45 }, "<")
      .to("#green-icon-group", 0.4, { x: 0 }, "<.2")
      .to("#blue-icon-group", 0.4, { y: -48 }, "<")
      .to("#red-icon-group", 0.4, { x: 0 }, "<.4")
      .to("#yellow-icon-group", 0.4, { y: 0 }, "<")
      .to("#green-icon-group", 0.6, { y: -48 }, "<.4")
      .to("#blue-icon-group", 0.6, { x: -45 }, "<")
      .to("#red-icon-group", 0.8, { y: 96 }, "<.6")
      .to("#green-icon-group", 1, { y: 0 }, "<.8");

    tl.current.pause();
  }, []);

  function handleMouseEnter() {
    tl.current.restart();
  }

  return (
    <svg
      className={styles.svg}
      onMouseEnter={handleMouseEnter}
      xmlns="http://www.w3.org/2000/svg"
      width="587"
      height="305"
      fill="none"
      viewBox="0 0 587 305"
    >
    ...

One thing that's different in my code is that I put the duration of each tween outside of the vars object. The second parameter can also be used to set a tween's duration. It's a little less typing, and a cleaner vars object.

Other than that, everything should work fine! Be sure to check out my posts on GSAP's motion path and scroll trigger too.

LinkedIn iconGithub icon