Maximizing the Power of Framer Motion with useAnimationControls

Maximizing the Power of Framer Motion with useAnimationControls

Featured on Hashnode

For most animations, we create variants for different animation states and plug those into the animate prop of our motion component. animate controls which variant the component animates to. It is triggered automatically when the component loads or the animate prop changes. We can use conditional logic or state to change the variant in animate.

This is great, but what if we wanted to chain those animations and move from variant to variant? What if we're going to time precisely when these animations happen? What if we want to compose animations on different components?

For that, we need more...

csi

CONTROL.

Why use animation controls?

We discussed how to create animation sequences using keyframes in the previous article. However, there were several drawbacks to keyframes, namely:

  • Adding pauses is painful

  • Composition drives complexity

  • Useless keyframes

Animation controls solve these problems and open new ways to integrate our animations into a living and breathing app. Let's jump right in!

useAnimationControls()

Framer motion exposes a hook called useAnimationControls(). It returns an object we can pass to our component's animate prop. Using this controls object, we can manually start, stop and schedule animations. Let's set up our component and see what it can do.

import { motion, useAnimationControls } from "framer-motion";
import { useEffect } from "react";

const divVariants = {
  left: {
    x: -10,
    transition: {
      duration: 1
    }
  },
  right: {
    x: 10,
    transition: {
      duration: 1
    }
  }
};

export default function Loader() {
  const divControls = useAnimationControls();

  useEffect(() => {
    // Using properties
    divControls.start({
      x: 20,
      transition: {
        duration: 2
      }
    });

    // Using variants
    divControls.start("left")
    divControls.set("right");
    divControls.stop();
  }, []);

  return (
  <motion.div 
  variants={divVariants}
  animate={divControls} />
  )
}

In the code above, we import the useAnimationControls hook, create variants and define our Loader component.

In the loader component we call the hook to get a controls object. We add the controls to our motion component's animate prop and our variants to the variants prop.

Control functions

The controls object has 3 methods we can use:

  • start: triggers the animation. We can provide an animation target directly using properties or the name of the variant we wish to animate to.

  • set: instantly sets the new values without animating. (works the same as duration: 0)

  • stop: stops the currently running animation

These three functions allow us to trigger our animations imperatively. We can start and stop animations from an event handler or tie them into our component's lifecycle using useEffect(). This also has some benefits we will get to in the next section.

If you are unfamiliar with useEffect(), check out this excellent Jack Herrington tutorial. He is the Bob Ross for React, and I can't recommend his videos highly enough.

Imperative animations

It can be beneficial to have imperative animations at your fingertips. For instance, when the user presses "like", you might want to animate a heart icon. We could add a click handler function and use controls to trigger our animation.

// Heart component on a blog article
export default function Heart() {
    const divControls = useAnimationControls();

    const handleClick = () => {
        divControls.start({
        scale: [1, 2, 1], // Using Keyframes to grow and shrink the heart
        transition: {
            duration: 0.5
        }
    });
    }

    return (
        <motion.div 
        animate={divControls} 
        onClick={handleClick} />
    )
}

This can be useful for fine-grained control over a component's animations. Using conditional logic within the click handler allows us to trigger different animations based on various conditions of our component.

Lifecycle animations

Complete manual control is sometimes handy, but what about linking the animations directly to the component's state? This can easily be achieved with the useEffect() hook.

// Heart component on a blog article
export default function Heart() {
    const [isLiked, setIsLiked] = useState(false);
    const divControls = useAnimationControls();

    useEffect(() => {
        divControls.start({
            scale: [1, 2, 1], // Using Keyframes to grow and shrink the heart
            transition: {
                duration: 0.5
            }
        });
    }, [isLiked]); // Depending on isLiked state

    return (
        <motion.div 
        animate={divControls} />
    )
}

By adding the isLiked variable to the dependency array, React will re-run useEffect() every time the variable changes. We can use this to trigger the animation automatically every time the user likes or un-likes.

What's great about this is that the animation does not rely on a click event but is directly tied to the component state. That means changing the isLiked state of our heart component from somewhere else will also trigger the animation. For example, if you have multiple items selected and wish to like them all, simply changing their state will animate all hearts simultaneously. Extremely useful stuff!

Control multiple components

The same control object can be used on multiple different motion components. Plug in the control object to the animate prop like so:

return (
    <>
        <motion.div 
        variants={divVariants}
        animate={commonControls} 
        />
        <motion.p 
        variants={pVariants}
        animate={commonControls} 
        />
    </>
    )
}

This will make all components controllable from a single controls object!

Note that each component will use its own variants. Calling commonControls.start("enter") will try to animate both components to their respective "enter" variant. So if you use the same controls on multiple components, make sure to either use properties to animate or use the same variant names on both components.

The animate prop is inherited if you do not define an animate prop on child components. This also applies to controls. That means you could add variants to your child's components and control them with the parent's controls object. This way, you could control the animations of a whole component tree in one go. Potent stuff!

If you do not want this behaviour, you can set the child's animate prop to any other value.

Sequences

We can manually control when the animations run, but how do we chain them?

useEffect(() => {
    const sequence = async () => {
        await divControls.start("left"};
        divControls.start("right"};
    }

    sequence();
});

To create a sequence, we create an async function within useEffect() and call it. This will run all of our control functions. But that's not all. The tasks on our control variable return a promise. Making the function asynchronous allows us to await the completion of the previous animation before we start the new one. In the example above, the "right" animation will only play after the "left" animation has finished.

Remember, we are still setting the animate prop for a single motion component. That single component can only perform one animation at a time. To animate multiple properties of that single component, you must create variants to reflect that.

Sequences within sequences

Consider the following code:

useEffect(() => {
    const sequence1 = async () => {
        await divControls.start("left"};
        await divControls.start("right"};
    }
    const sequence2 = async () => {
        await divControls.start("up"};
        await divControls.start("down"};
    }
    const sequenceJoined = async () => {
        await sequence1();
        await sequence2();
    }

    sequenceJoined();
});

sequenceJoined contains a sequence that first runs sequence1 and then runs sequence2. This means that when sequenceJoined is called, the motion component will first animate to the "left" variant, then to the "right" variant, then to the "up" variant, and finally to the "down" variant.

This demonstrates how you can nest sequences within sequences to create complex animation chains. This technique allows for more composability in designing your animations while retaining complete control over the timing and order.

Conclusion

Before your head starts spinning with all the possibilities, let's call it a day.

To sum it up, useAnimationControls() is extremely useful when you need to take things into your own hands. You can define the behaviour of your animations exactly how you want by creating sequences or tying animations into the component lifecycle.

Thank you for reading! Feel free to ask me any questions.

Did you find this article valuable?

Support Noël Cserépy by becoming a sponsor. Any amount is appreciated!