Framer Motion: Start an Animation on Hover
The motion
component of framer motion has a whileHover
prop which allows you to declare a state that the component is animated to, while the cursor is hovering over it.
This is great for signaling interactivity to your user. Buttons can scale up, box shadows can grow and links can change color.
If, however, your animation is composed of several states and should keep playing after the hover event, you’ll need to go a litte further.
useAnimation()
To start the animation, we need to get a handle of the animation-instance itself. Have a look at the following code:
//import {motion, useAnimation} from 'framer-motion'
const divAnimationControls = useAnimation()
;<motion.div animate={divAnimationControls}></motion.div>
Our soon-to-be-animated div has only one animate
prop where we pass in the divAnimationControls
. This gives us access to the .start()
, .stop()
and .sequence()
methods.
We can now call divAnimationControls.start()
from anywhere in our component to start the animation.
Animation Variants
Well, we can now tell the div
to start the animation, but it doesn’t know what to animate.
This is where animation variants come into play. In our component, we add a plain old JS-object like that:
const divAnimationVariants = {
init: {
y: 0,
},
anim: {
y: -20,
},
}
With that sorted, we can now tell our <motion.div>
to start the animation when the user starts hovering:
<motion.div
animate={divAnimationControls}
onHoverStart={() => {
logoAnimationControls.start(divAnimationVariants.anim)
}}
></motion.div>
The onHoverStart
-prop takes in a function. Within that function we take our logoAnimationControls
and call the .start()
method with the argument being the one it should animate to (divAnimationVariants.anim
).
And it works! If you now hover over your element, it moves up and stays there.
If you want your element to return to the initial position, update your divAnimationVariants
like that:
const divAnimationVariants = {
init: {
y: 0
},
anim: {
y: -20
transition: {
type: "tween",
repeat: 1,
repeatType: "reverse",
},
}
}
*But, there is one problem*.
The problem
If your user is of the impatient type or your animation very long, chances are, that they hover over the element again before the first cycle is complete. This will have some quirky, unexpected results depending on the animation.
To combat this, we have to rely on reacts good ol’ useState
.
//import {useState} from 'react'
const [isAnimationPlaying, setIsAnimationPlaying] = useState(false)
Initially the animation doesn’t play, so we set the value to false
.
Then we only have to update our onHoverStart
function and add an onAnimationComplete
prop like so:
<motion.div
animate={divAnimationControls}
onHoverStart={() => {
if (!isAnimationPlaying) {
setIsAnimationPlaying(true)
logoAnimationControls.start(divAnimationVariants.anim)
}
}}
onAnimationComplete={() => {
setIsAnimationPlaying(false)
}}
></motion.div>
The onAnimationComplete
prop also takes in a function, where we are just setting the isAnimationPlaying
state to false
, allowing it to be played again with the next hover.
And that’s it, folks. If you were lost on the way down here, or just want the complete answers, check below:
Full code
import React, {useState} from 'react'
import {motion, useAnimation} from 'framer-motion'
const AnimatedComponent = () => {
const [isAnimationPlaying, setIsAnimationPlaying] = useState(false);
const divAnimationControls = useAnimation();
const divAnimationVariants = {
init: {
y: 0
},
anim: {
y: -20
transition: {
type: "tween",
repeat: 1,
repeatType: "reverse",
},
}
}
return(
<motion.div
animate={divAnimationControls}
onHoverStart={() => {
if (!isAnimationPlaying) {
setIsAnimationPlaying(true)
logoAnimationControls.start(divAnimationVariants.anim)
}
}}
onAnimationComplete={() => {
setIsAnimationPlaying(false)
}}
>I go up and down!</motion.div>
)
}