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
hookmotion
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"
>

</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'>

</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.