Sometimes a single movement is simply not enough. Sometimes you want to emphasize an action, draw attention, or even build complex animations with multiple components. If you want to really sell the effect, an animation sequence is just what you need.
Simple animations might go shwoop 💨
But with sequences, they can go shwoop BAM! 💨💥
And who doesn't want that? Am I right?
See for yourself
Below is a button to open and close a menu. This simple action can feel a lot smoother if we use a sequence. Click on the orange "open" text in the CodeSandbox below to see what I mean.
Our simple menu button became a joy with just a few lines of code. Feel free to look at the code by swiping the handle on the left. By the end of this article, you will understand precisely how it all works.
Let's get started!
In Framer Motion, there are several ways to create such animation sequences. All methods work differently, and each method has its pros and cons. Today we will dive into the first method: keyframes.
I will be using variants, transitions, looping and easing functions liberally in this article. Click the links if you don't know what they are or need a quick refresher. They will take you to my previous article, where I explain those concepts in more detail.
What are keyframes?
This concept will be familiar if you are acquainted with animation or video editing. Keyframes are used to define the most essential or most extreme poses and positions in an animation. Using keyframes as start and end points, animators would add additional in-between frames to create smooth transitions.
Framer Motion also allows us to use keyframes to define an animation sequence. They are a fantastic tool for creating looping animations and short movements. They can be integrated into other sequences and have a clever trick up their sleeve for switching between animations.
Using keyframes
Typically when we use the animate
prop, we provide a single value for the property we wish to animate, e.g. x: 10
. This will act as the destination.
To create a keyframe sequence, we can instead provide an array of values.
import { motion } from "framer-motion";
const variants = {
slide: {
x: [0, 200, 0]
}
}
export default function Loader() {
return (
<motion.div
variants={variants}
animate="slide" />
)
}
Framer Motion will automatically detect that we are using keyframes and animate our x
value from 0
to 200
and back to 0
. This method works without specifying anything else because of the convenient default values Framer Motion uses.
Basic examples
Luckily, we don't need to draw any in-between frames by hand to create smooth transitions. Framer Motion handles that for us. Let's look at some examples and see how we can use the transition
options for different effects.
Default
Only the keyframes array and duration
has been set here. The keyframes are spread evenly over the total duration. Each transition between keyframes takes the same amount of time to complete.
transition: {
duration: 1
}
Times
If we want more control over the transition timing, we can use the times
option. times
is an array of numbers ranging from 0
to 1
. One number for every keyframe. Each number defines when the corresponding key frame should be reached in the animation.
Important: The length of the
times
array has to be equal to the length of the keyframe array.
In the example, the component starts at x: 0
. It reaches x: 200
after 0.8 seconds and then returns to x: 0
by 1 second. If we double our duration
to 2 seconds, these times would also double.
transition: {
times: [0, 0.8, 1]
}
Ease
Defining a single value such as ease: "easeIn"
will apply that easing function to all transitions between keyframes. Not setting an ease
value will default to ease: "easeInOut"
.
If we add an array of easing functions, we can assign a different easing function to every transition. Note that the length of our easing function array must equal the number of transitions. That means the number of keyframes minus one.
transition: {
ease: ["easeIn", "easeOut"]
}
Null
In all of our previous examples, triggering the animation forces x
to snap to the first keyframe, regardless of its current position. That means if we begin a new animation while the component is already moving, it snaps instantly to the starting position of the new animation. This can look jarring in certain conditions.
When we set the first keyframe to null
, it will act as a wildcard value. null
will be replaced by the component's current value when the animation starts. This lets us transition smoothly between animations. This technique is convenient when we implement drag animations or other gestures.
transition: {
x: [null, 200, 0]
}
Advanced examples
Jumping ball
I like this example because it shows us a clever and concise way of using keyframes.
The ball jumps up and down using only y: [0, -70]
. The ball looks like it is being affected by gravity because we apply ease: "easeOut"
. This will slow down the ball on the way up. But that is only half of the animation. What about the rest?
Now comes the clever bit. We are repeating this animation with repeatType: "reverse"
. This will play the animation backwards. The ball slows down on the way up, reaches the apex and then speeds up on the way down. This also demonstrates what the different values for repeatType
do. If we had used "mirror"
, the ball would descend quickly at first and slow down before it reached the ground.
This animation demonstrates another aspect of keyframes. Namely, we can store a whole animation sequence within one variant. You can switch between two animation sequences if you click on the button. This is useful when we want to pack interesting interactions into a small footprint.
You can fork the CodeSandbox and try changing the ease
and repeatType
options to better understand this.
Tumbler
In the second example, I use almost all of the abovementioned techniques. My goal here was to make a domino that keeps falling forward. It is a simple loading animation, yet it shows some of the most significant limitations when using keyframes.
The animation starts with the box standing tall in the middle of the platform. I set originX: 1
and originY: 1
to move our transform origin to the bottom right. This allowed me to rotate the box around that corner to mimic falling.
Because the box is 80px tall and 20px wide, tipping it to the side twice will move it by 100px. Thus, I use x: -100
to slide the box to the left to keep it in the same place.
Because the shape tips on its side, the transform-origin is now on the bottom left. Yet, to tip the shape upright again, I need to rotate it around the bottom right. All the movements I have done thus far have been in relation to my original transform-origin. Moving the transform-origin is problematic because it will change all my previous transformations.
My solution was to swap the component for a new one using opacity
. The new component starts lying down and has its origin on the bottom right. I can then tip the new component to its vertical position and swap it back to complete the loop.
After all this, you are probably wondering where the loop even began. Even if you look at the code, you might think, "😨 My goodness! This looks frightfully hacky!"
And you would be right!
Let's talk about it.
Drawbacks of keyframe sequences
Although keyframes can create great-looking sequences, there are some concerns about usability.
Useless keyframes
In many cases, we want to animate multiple properties of our component (e.g. opacity
and x
). We can simply add the keyframes for both properties.
variant: {
opacity: [0, 1],
x: [100, 0],
}
But the whole point of a sequence is that things happen one after the other. Shwoop BAM! 💨💥, remember? Meaning, not all properties are animating at the same time.
If we want to animate opacity
before x
, we need to add useless keyframes to every property we are not currently animating.
variant: {
opacity: [0, 1, 1],
x: [100, 100, 0],
}
This forces us to duplicate keyframes. This isn't a problem for simple animations but adds up quickly when we are animating many properties with many keyframes.
Composition drives complexity
Managing multiple components with keyframes is a pain. The reason is timing.
The times
option uses fractions of the total duration. This is handy if we animate one component, but what about two? Suppose we try to start two animations from different components simultaneously. In that case, we need to calculate the start time by multiplying the total duration by the times
value of the given keyframe. Confusing, right?
I altogether avoided that in the Tumbler example by using the same duration for both components and matching the times
values. But this brings us back to adding useless keyframes.
And what if we want to change the timing? We need to go into every property array and every times
array to update values. Keeping track of things like this turns into a nightmare in no time.
Adding pauses is painful
To add a pause, you must duplicate all properties' keyframes and then define a new time
value.
variant: {
x: [100, 0, 0, 100],
transition: {
times: [0, 0.4, 0.6, 1]
}
}
Needless to say, this is not ideal, especially if you have many properties.
The better way
This article covered creating keyframe animations and using the various transition
options. With the help of examples, we discussed keyframe animations' uses and limitations. This ended the article on a rather glum note. But what if I told you there was a better way?
Join me in part 2, where we will explore useAnimationControls()
and solve some of the issues we have with keyframes.