Handle animation state with Framer Motion
October 12, 2020
I recently redesigned this very website and used the opportunity to deeper explore a few topics (testing, typescript...), including animations. After trying a few animation libraries for React, I settled on Framer Motion as it is both beginner-friendly and extremely powerful, with a growing community supporting the project.
However, I found the documentation rather difficult to explore due to the lack of a search bar. One thing I had to figure out was how to trigger and stop successive animations.
If you navigate to my homepage, you should see a color wheel fading in on page load then pulsing once. My first few tries could solve the fact that both animations were happening simultaneously. I found in the docs a transition property called staggerChildren but couldn't make it work.
Another solution would have been to play with keyframes but this required quite a bit of fine tuning in terms of duration and delays.
So I decided to layer my animations and rely on good ol' React state.
The first step is to conditionally set your pulse animation based on the pulseOnce state you've defined.
Framer Motion then gives you access to an event listener called onAnimationComplete. Once my first animation completed, it sets pulseOnce to true, triggering the second animation.
The process repeats itself once the "pulse" animation finished, bringing back the state to its original value.
1import React, {useState} from "react"
2import {motion} from "framer-motion"
3
4const AnimatedElement = () => {
5 const [pulseOnce, setPulseOnce] = useState(false)
6
7 return (
8 <motion.div
9 variants={{
10 hidden: {
11 opacity: 0,
12 scale: 1.3,
13 },
14 visible: {
15 opacity: 1,
16 transition: {
17 delay: 0.8,
18 },
19 },
20 }}
21 initial="hidden"
22 animate="visible"
23 onAnimationComplete={() => setPulseOnce(true)}
24 >
25 <motion.div
26 variants={{
27 pulse: {
28 scale: [1, 1.1, 1, 1.2, 1, 1],
29 transition: {
30 delay: 1,
31 },
32 },
33 }}
34 animate={pulseOnce ? "pulse" : null}
35 onAnimationComplete={() => setPulseOnce(false)}
36 whileHover={{ scale: 1.2 }}
37 >
38 <ColorWheel />
39 </motion.div>
40 </motion.div>
41 )
42}
43
44export default AnimatedElement