Animate on Button Click | Motion, NextJS

In this article, we will explore how to animate elements on a button click using the motion library (formerly known as framer-motion). We will demonstrate this with examples that use:

  • useAnimate hook

  • motion elements from the library

Install motion and next-js in your project. The code snippets in the example use TypeScript and TailwindCSS.

Animating Using motion Elements:

Using the motion elements from the motion library, we can animate an element when it is tapped by applying the whileTap property on the element. This property defines the animation to play while the user holds the click.

<motion.button
    variants={{
        initial: { scale: 1 },
        hovered: { scale: 1.02 },
        tapped: { scale: 0.98 }
    }}
    initial="initial"
    whileHover="hovered"
    whileTap="tapped"
>
    {/* ...content of your button */}
</motion.button>

We can also use the same variant names on the parent element to trigger animations on child elements. For example:

<motion.button
  variants={{
    initial: { scale: 1 },
    hover: { scale: 1.02 },
    click: { scale: 0.98 },
  }}
  transition={{ duration: 0.4 }}
  initial="initial"
  whileHover="hover"
  whileTap="click"
  className="px-3 py-1 shadow-md text-purple-200 bg-black dark:bg-white dark:text-purple-400 rounded-xl flex items-center gap-1.5"
>
  <motion.span
    variants={{
      initial: { rotate: -10 },
      hover: { rotate: -30 },
      click: { rotate: -360 },
    }}
    transition={{ duration: 0.5 }}
    className="material-symbols-outlined !text-base"
  >
    &#xe042;
  </motion.span>
  Restart
</motion.button>

In this example, clicking the button triggers animations for both the button and its child element. The child element uses the same variant names (initial, hover, click) defined in the parent to synchronize the animations.

However, with this approach, the animation will play only as long as the mouse click is held. Releasing the mouse stops the animation, potentially leaving it incomplete.

Animating Using the useAnimate Hook:

Now, let’s see how we can animate a button when it is clicked using the useAnimate hook. The useAnimate hook provides a scope ref and an animate function, enabling animation of the referenced elements.

Here’s an example:

export function ButtonExample() {
  const [scope, animate] = useAnimate();

  return (
    <button
      ref={scope}
      onMouseEnter={() => animate(scope.current, { scale: 1.02 })}
      onMouseLeave={() => animate(scope.current, { scale: 1 })}
      onClick={async () => {
        await animate([
          [scope.current, { scale: [0.98, 1] }],
          ['.restart-button__icon', { rotate: -360 }, { at: '<' }],
          ['.restart-button__icon', { rotate: 0 }, { duration: 0.000000001 }],
        ]);

        // ... statements
      }}
      className='absolute bottom-5 right-5 z-20 px-2.5 py-1.5 shadow-md text-purple-custom dark:text-purple-200 rounded-xl flex items-center gap-1.5 border-2 border-gray-100/75 dark:border-gray-500/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-purple-custom/80 focus-visible:ring-offset-white dark:focus-visible:ring-offset-black'
    >
      <span className='restart-button__icon material-symbols-outlined !text-base'>
        &#xe042;
      </span>
      Restart
    </button>
  );
}

The scope ref needs to be attached to the DOM node containing the element to be animated. The animate function takes arguments in the form selector, animation-properties, transition-properties.

From the code snippet above, we attach the reference to the element we want to animate. All children of that referenced element can also be animated. Use the animate function with an element selector as the first parameter, followed by an object containing the animation properties. Optionally, a third object specifies the transition properties.

In the example, the at property in the third argument is used:

await animate([
  // ...
  ['element selector here', { /* animation properties here */ }, { at: '<' }],
  // ...
]);

This property synchronizes animations to run concurrently with the previous animation. Read more in the docs: Timeline Sequences.

Additional Resources