Guide to creating animations that spark joy with Framer Motion
December 15, 2020 / 15 min read
Last Updated: January 31, 2023Over the past few months, Framer Motion went from being a fun tool I played with on the side to a core element of my frontend projects when it comes to adding a layer of interaction to my UIs. I went from knowing almost nothing about animations and transitions, to being able to orchestrate more complex animations involving lots of elements.
I've shared a lot of the animation work I sprinkled throughout my blog on Twitter, and a lot of you have asked me to share more code snippets. Thus I felt it was time for a little write-up!
In this post, you'll find a condensed guide containing everything I've learned when it comes to Framer Motion, the key concepts of animation, and how to use this library to create animations that spark joy through some interactive examples and widgets.
Anatomy of an animation
First, let's take a look at the main elements that define an animation. When working on one, whether it's to move an element, changing its shape, or color, I always try to answer the following 3 questions:
- "Where/how is my element at the beginning?" i.e the initial state
- "Where it needs to go or which shape it needs to take by the end?" i.e. the target state
- "How it's going to transition from the initial state to the end state?" i.e. the transition state
In the case of Framer motion, the library gives us a motion
component which takes 3 properties (props) that let us define an answer to the 3 questions above:
initial
: the state of our element at mount time.
1<motion.div2...3initial={{4x: 0,5rotate: 45,6}}7...8/>
animate
: the state in which our element will be at the end of the animation.
1<motion.div2...3animate={{4x: 50,5rotate: 270,6}}7...8/>
transition
: how our element goes from the initial state to the target state. This is where we can define which transition type we want to define, delays, or repetitions of the same transition.
1<motion.div2...3transition={{4ease: "easeIn",5duration: 0.7,6}}7...8/>
There are many types of transitions available in Framer Motion so I added this little comparative visualization below for you to see the little nuances between some of the main types and tweak their respective options:
1<motion.div2...3transition={{4type: 'spring',5stiffness: 100,6mass: 3,7damping: 1,8}}9/>10
1<motion.div2...3transition={{4type: 'tween',5ease: 'easeInOut',6duration: 2,7...8}}9/>10
1<motion.div2...3transition={{4type: 'inertia',5velocity: 50,6}}7/>8910
You can find the complete list of types and all their respective options in this section of the documentation.
Now that we went through the basics, let's take a look at our first examples! Below you will find a series of animated components that you can edit and tweak at will. As for what to tweak, the following list contains a few interesting points that you can check out:
- **remove the **
transition
prop from the first component (Example1). Notice that this translation animation went from anease
type to aspring
type. This comes from the "smart defaults" we just mentioned. - combine animations in Example2: change the second animation from a simple rotation to a rotation and a translation.
I added hints in the comments of the code to guide you. 😄
import { motion } from 'framer-motion'; import './scene.css'; const Example1 = () => { return ( <div style={{ marginBottom: '50px' }}> <p>Example 1</p> <motion.div style={{ background: 'linear-gradient(90deg,#ffa0ae 0%,#aacaef 75%)', height: '100px', width: '100px', borderRadius: '10px', }} /** Below, the initial and animation field are set to declare a translation animation along the horizontal axis "x" Hence why we're setting an "x" field in both objects. **/ initial={{ x: -100, }} animate={{ x: 100, }} /** The code below specifies the transition type for our element. You can comment the whole transition prop below, and Framer Motion will fallback to "smart defaults". In this case, since we have a translation, the default transition type is spring, so you should see the element moving from left to right and "bounce" a when reaching its target state, like a spring! **/ transition={{ type: 'tween', ease: 'easeInOut', repeat: Infinity, repeatType: 'reverse', repeatDelay: 1, duration: 2, }} /> </div> ); }; const Example2 = () => { return ( <div style={{ marginBottom: '50px' }}> <p>Example 2</p> <motion.div style={{ background: 'linear-gradient(90deg,#ffa0ae 0%,#aacaef 75%)', height: '100px', width: '100px', borderRadius: '10px', }} /** Combining animations in Framer Motion is very easy! You can simply add extra fields to your initial and target object. Here for example, our element rotates between 0 and 180 degrees, if we want to have it translate horizontally at the same time, we can simply add an "x" field, like in the example above. I added these fields below, commented. If you uncomment them, you should see our element both rotate and translate at the same time. You can try changing the translation from horizontal to vertitcal, by replacing the "x" field with an "y" field. **/ initial={{ rotate: 0, // x: -100 }} animate={{ rotate: 180, // x: 100 }} transition={{ type: 'tween', ease: 'easeInOut', repeat: Infinity, repeatType: 'reverse', repeatDelay: 1, duration: 2, }} /> </div> ); }; const Examples = () => ( <div> <Example1 /> <Example2 /> </div> ); export default Examples;
Using variants
Now that we've seen and tweaked our first Framer Motion based components, you might notice that, in the case of complex animations, things can quickly get messy. Defining everything inline can lead to your motion components being fairly hard to read but also a bit repetitive.
This is why one of my favorite features of Framer Motion is the ability to define animations in a declarative way through variants.
Variants are sets that have predefined animation objects, the kind of object we passed in the examples above in the animation
prop.
The following is an example showcasing how you can leverage variants. Notice how we declared a set of variants within the buttonVariants
object and how the respective keys of these variants are referenced in the motion component:
Using variants with the motion component
1import { motion } from 'framer-motion';23const AnimatedButton = () => {4const buttonVariants = {5hover: {6scale: 1.5,7},8pressed: {9scale: 0.5,10},11rest: {12scale: 1,13},14};1516return (17<motion.button18initial="rest"19whileHover="hover"20whileTap="pressed"21variants={buttonVariants}22>23Click me!24</motion.button>25);26};
After seeing these variants the first time, like me, you might be wondering "wait, if everything is predefined, how can I make my animations based on some dynamic property?"
Well, don't you worry! Framer Motion lets you also define variants as functions. Each variant as a function can take one argument and return and animation object. That argument has to be passed in the custom
prop of your motion component.
The example below showcases an example of variant as function, the hover variant will return a different object whether the button is clicked or not. The state of the button isClicked
is passed in the custom
prop of the motion component.
Using variants and the custom prop with the motion component
1import { motion } from 'framer-motion';23const AnimatedButton = () => {4const buttonVariants = {5// any variant declared as a function will inherit the `custom prop` as argument6hover: (clicked) => ({7// once clicked the button will not scale on hover anymore8scale: clicked ? 1 : 1.5,9}),10pressed: {11scale: 0.5,12},13rest: {14scale: 1,15},16};1718const [clicked, setClicked] = React.useState(false);1920return (21<motion.button22initial="rest"23whileHover="hover"24whileTap="pressed"25variants={buttonVariants}26custom={clicked}27onClick={() => setClicked(true)}28>29Click me!30</motion.button>31);32};
Now that we know what variants are, let's try to work with them in the following playground. Let's try to:
- make the first button scale on hover (for now, it only rotates).
- make the button not scale back to its original size if it's been clicked on. Hint: you can use the
custom
prop we just mentioned above 💡.
Like in the first part, I left comments in the code to guide you!
import { motion } from 'framer-motion'; import React from 'react'; import './scene.css'; const Example = () => { const [isClicked, setIsClicked] = React.useState(false); React.useEffect(() => { if (isClicked) { setTimeout(() => setIsClicked(false), 3000); } }, [isClicked]); const duration = 0.6; const buttonVariants = { hover: { /** * Combining different animation in variants works the same way it works * for inline animation objects * * For the first example, to make the button scale, you simply have to * uncomment the following. Once done, hover the button and notice how * it now double in size! */ // scale: 2, rotate: 360, }, pressed: { scale: 0.95, }, clicked: { scale: 1, }, notClicked: { scale: 1, }, }; /** * Comment the buttonvariants object above and * uncomment the one below to try out the second * example: * * - the button will not scale back to its basic size once clicked * - once clicked, the hover animation will not happen. It will use * the "isClicked" custom prop passed to the button component below */ /* const buttonVariants = { hover: (isClicked) => ({ scale: isClicked ? 2 : 3, rotate: isClicked ? 0 : 360, }), pressed: { scale: 0.95, }, clicked: { scale: 2, }, notClicked: { scale: 1, }, }; */ return ( <motion.button style={{ background: 'linear-gradient(90deg,#ffa0ae 0%,#aacaef 75%)', color: 'black', border: 'none', height: '50px', width: '200px', borderRadius: '10px', cursor: isClicked ? 'default' : 'pointer', outline: 'none', boxShadow: '6px 4px 12px -6px rgba(0,24,40,0.25)', }} aria-label="Click Me!" title="Click Me!" onClick={() => { setIsClicked(true); }} /** * Here we pass the buttonVariants object as variants. It contains 4 * different target objects * - hover: which is used for the whileHover prop * - pressed: which is used for the whileTap prop * - clicked and notClicked which are respecively used for animate prop * when the button is clicked and not clicked (based on the state of the * button) * * Reference to these animation objects are passed as strings to their * props * * e.g. whileHover="hover" */ variants={buttonVariants} animate={isClicked ? 'clicked' : 'notClicked'} whileHover="hover" whileTap="pressed" /** * Uncomment the following to allow our buttonVariants objects to know * about the status of the button. * * This lets us redefine variants based on the status button */ // custom={isClicked} transition={{ duration, }} > {isClicked ? 'Clicked!' : 'Click Me!'} </motion.button> ); }; export default Example;
Advanced animations using Motion Values
At this point, we know how to use the key features of Framer Motion to start building our own animations:
- we know the main elements that define an animation ✅
- we know how to use variants to define animations in a declarative way ✅
With those newly acquired skills, we can now look at on more concept that will allow us to build more advanced animations: Motion Values. In this part we will learn what are Motion Values and how to use them and also looked at a practical example to illustrate this concept: my own "Copy To Clipboard" button!
Motion Values
A MotionValue is an internal value to the Framer Motion library that "tracks the state and the velocity of an animating value".
For more complex animation we may want to create our own MotionValue (quote from the docs), and then add them as inline style to a given component. To define a MotionValue, we need to use the useMotionValue
hook.
A MotionValue can be practical when you want to have one animation depending on another one. For example, we may want to tie together the scale and the opacity of a component in such a way that, once the component reaches half of its targeted scale, the opacity should be equal to 100%.
To handle that kind of use case, Framer Motion gives us a second hook: useTransform
that transforms an input MotionValue to another MotionValue through a function. The example below showcases how you can use these 2 hooks together:
import { motion, useMotionValue, useTransform } from 'framer-motion'; import './scene.css'; const Example = () => { const blockVariants = { initial: { rotate: 0, }, target: { rotate: 360, }, }; const rotate = useMotionValue(0); /** * Here we tie together the value of "scale" to the value * of "rotate" * The scale will increase along the rotation, from 0 * until the rotation reaches 270 degrees ([0, 270]) * where the scale property will be equal to 1 ([0, 1]). * The scale will stop increasing while the rotation * finishes its transition * * You can try to modify the values below, and see how it * impacts the resulting transition. */ const scale = useTransform(rotate, [0, 270], [0, 1]); return ( <motion.div style={{ background: 'linear-gradient(90deg,#ffa0ae 0%,#aacaef 75%)', height: '100px', width: '100px', borderRadius: '10px', rotate, scale, }} variants={blockVariants} initial="initial" animate="target" transition={{ ease: 'easeInOut', duration: 4, }} /> ); }; export default Example;
Dissecting the "Copy To Clipboard" animation
You might have noticed that I sprinkled some animated SVG icons for my buttons throughout my blog ✨. One of my favorite is the "Copy To Clipboard" button on my code snippets, so I figured it would a great case study to look at together to illustrate some of the use cases for Motion Values.
It uses both useMotionValue
and useTransform
to ensure that the opacity
level of our checkmark icon is a function of its pathLength
.
I added a "dissected" version of this component below to let you fully understand what is happening when clicking on the icon and how the Motion Values change throughout the transition. You can tweak the duration with the slider, and also visualize the MotionValue
for the opacity and pathLength of the checkmark SVG.
When clicking on the button, you can see that the more the pathLength increases, the more the opacity of the checkmark increases as well following this function:
1f: y -> x * 223// Where x is the pathLength of our SVG y is the opacity
which is equivalent to the following code using Framer Motion's hooks:
1const pathLength = useMotionValue(0);2const opacity = useTransform(pathLength, [0, 0.5], [0, 1]);
Here's the code for the full implementation of this component:
Full implementation of the Copy To Clipboard button animation
1import React from 'react';2import { motion, useMotionValue, useTransform } from 'framer-motion';34const CopyToClipboardButton = () => {5const duration = 0.4;67const clipboardIconVariants = {8clicked: { opacity: 0 },9unclicked: { opacity: 1 },10};1112const checkmarkIconVariants = {13clicked: { pathLength: 1 },14unclicked: { pathLength: 0 },15};1617const [isClicked, setIsClicked] = React.useState(false);1819const pathLength = useMotionValue(0);20const opacity = useTransform(pathLength, [0, 0.5], [0, 1]);2122return (23<button24css={{25background: 'transparent',26border: 'none',27cursor: isClicked ? 'default' : 'pointer',28outline: 'none',29marginBottom: '20px',30}}31aria-label="Copy to clipboard"32title="Copy to clipboard"33disabled={isClicked}34onClick={() => {35setIsClicked(true);36}}37>38<svg39width="100"40height="100"41viewBox="0 0 25 25"42fill="none"43xmlns="http://www.w3.org/2000/svg"44>45<motion.path46d="M20.8511 9.46338H11.8511C10.7465 9.46338 9.85107 10.3588 9.85107 11.4634V20.4634C9.85107 21.5679 10.7465 22.4634 11.8511 22.4634H20.8511C21.9556 22.4634 22.8511 21.5679 22.8511 20.4634V11.4634C22.8511 10.3588 21.9556 9.46338 20.8511 9.46338Z"47stroke="#949699"48strokeWidth="2"49strokeLinecap="round"50strokeLinejoin="round"51initial={false}52animate={isClicked ? 'clicked' : 'unclicked'}53variants={clipboardIconVariants}54transition={{ duration }}55/>56<motion.path57d="M5.85107 15.4634H4.85107C4.32064 15.4634 3.81193 15.2527 3.43686 14.8776C3.06179 14.5025 2.85107 13.9938 2.85107 13.4634V4.46338C2.85107 3.93295 3.06179 3.42424 3.43686 3.04917C3.81193 2.67409 4.32064 2.46338 4.85107 2.46338H13.8511C14.3815 2.46338 14.8902 2.67409 15.2653 3.04917C15.6404 3.42424 15.8511 3.93295 15.8511 4.46338V5.46338"58stroke="#949699"59strokeWidth="2"60strokeLinecap="round"61strokeLinejoin="round"62initial={false}63animate={isClicked ? 'clicked' : 'unclicked'}64variants={clipboardIconVariants}65transition={{ duration }}66/>67<motion.path68d="M20 6L9 17L4 12"69stroke="#5184f9"70strokeWidth="2"71strokeLinecap="round"72strokeLinejoin="round"73initial={false}74animate={isClicked ? 'clicked' : 'unclicked'}75variants={checkmarkIconVariants}76style={{ pathLength, opacity }}77transition={{ duration }}78/>79</svg>80</button>81);82};
It might seem dense at first, but you'll notice that it is composed of elements that we've seen individually in the previous sections and examples:
- variants for the clipboard SVG and the checkmark SVG
1const clipboardIconVariants = {2clicked: { opacity: 0 },3unclicked: { opacity: 1 },4};56const checkmarkIconVariants = {7clicked: { pathLength: 1 },8unclicked: { pathLength: 0 },9};
useMotionValue
anduseTransform
to intertwine the opacity and pathLength values together
1const pathLength = useMotionValue(0);2const opacity = useTransform(pathLength, [0, 0.5], [0, 1]);
Orchestration
For this last part, we will focus on how to orchestrate animations, especially with the two types of orchestration I used the most when building animations:
- Delays and repetitions: "move to point A, then 2 seconds later move to point B then repeat"
- Parent-Children: "parent appears first, then the children one after the other at 1-second interval"
Delays and repetition
This is perhaps the first type of orchestration you'll naturally think about when starting to experiment with more complex animations. Framer Motion lets you not only delay when an animation should kick-off but also delay any repetition of that same animation if needed.
I used delays and repetitions to orchestrate some of the micro-animations you can see in my Guide to CI/CD for frontend developers which were the first fairly complex animated components I implemented.
A few orchestration patterns have already been showcased in some of the previous examples out of necessity, but here's a more detailed example for you to play with:
- you can try to change the repeat type from
mirror
toloop
and observe the subtle change of repetition type. - make the animation repeat indefinitely instead of just 3 times.
- make the initial delay 2s and every repeat delay 1s, you should observe the animation pausing between each repetition.
import { motion } from 'framer-motion'; import './scene.css'; const Example = () => { const blockVariants = { initial: { y: -50, }, target: { y: 100, }, }; return ( <motion.div style={{ background: 'linear-gradient(90deg,#ffa0ae 0%,#aacaef 75%)', height: '100px', width: '100px', borderRadius: '50%', }} variants={blockVariants} initial="initial" animate="target" transition={{ ease: 'easeInOut', duration: 0.7, delay: 1, repeat: 3, // repeat: Infinity, repeatType: 'mirror', repeatDelay: 0, }} /> ); }; export default Example;
Parent-Children
A more advanced pattern for orchestration that I recently discovered is what I named "parent-children orchestration". It is pretty useful when you want to delay the animations of some children components in relation to an animated parent component.
Framer Motion gives us the delayChildren
option for our transition object to do just that:
Using delayChildren in a transition
1const boxVariants = {2out: {3y: 600,4},5in: {6y: 0,7transition: {8duration: 0.6,9// Both children will appear 1.2s AFTER the parent has appeared10delayChildren: 1.2,11},12},13};1415const iconVariants = {16out: {17x: -600,18},19in: {20x: 0,21},22};2324return (25<motion.div variants={boxVariants} initial="out" animate="in">26<motion.span27role="img"28aria-labelledby="magic wand"29variants={iconVariants}30>31🪄32</motion.span>33<motion.span role="img" aria-labelledby="sparkles" variants={iconVariants}>34✨35</motion.span>36</motion.div>37);
On top of that, what if we wanted to not only delay the children as a group but also delay each child based on its siblings, such as, make them appear 1s after their previous sibling appeared. Well, we're in luck, because there's an easy way to do that with the staggerChildren
Using delayChildren and staggerChildren in a transition
1const boxVariants = {2out: {3y: 600,4},5in: {6y: 0,7transition: {8duration: 0.6,9// The first child will appear AFTER the parrent has appeared on the screen10delayChildren: 1.2,11// The next sibling will appear 0.5s after the previous one12staggerChildren: 0.5,13},14},15};1617const iconVariants = {18out: {19x: -600,20},21in: {22x: 0,23},24};2526return (27<motion.div variants={boxVariants} initial="out" animate="in">28<motion.span29role="img"30aria-labelledby="magic wand"31variants={iconVariants}32>33🚀34</motion.span>35<motion.span role="img" aria-labelledby="sparkles" variants={iconVariants}>36✨37</motion.span>38</motion.div>39);
What these 2 options exactly do might seem confusing at first. I wished I had some visual examples to really get a grasp on how they worked when I got started. I hope the following visualization will do just that!
In the widget below, you can tweak the values of beforeChildren
and staggeredChildren
and see how the resulting transition.
I used this type of orchestration to power the list of people who've shared or liked my articles that you can see at the end of each blog post. It's a component that quite a few people like, so I thought I could use it as a little example for you to interact and have fun with:
import { motion } from 'framer-motion'; import './scene.css'; const Example = () => { const replies = [ { id: '1', photo: '🐶', }, { id: '2', photo: '🐱', }, { id: '3', photo: '🐰', }, { id: '4', photo: '🐭', }, { id: '5', photo: '🐹', }, { id: '6', photo: '🦊', }, { id: '7', photo: '🐻', }, { id: '8', photo: '🐼', }, { id: '9', photo: '🐨', }, ]; const list = { visible: { opacity: 1, transition: { // delayChildren: 1.5, staggerChildren: 0.1, }, }, hidden: { opacity: 0, }, }; const item = { visible: { opacity: 1, x: 0 }, hidden: { opacity: 0, x: -10 }, }; return ( <> <h4>Already {replies.length} furry friends liked this post!</h4> <motion.ul style={{ display: 'flex', flexWrap: 'wrap', marginLeft: '0px', marginBottom: '8px', marginTop: '15px', paddingLeft: '0px', }} initial="hidden" animate="visible" variants={list} > {replies.map((reply) => ( <motion.li style={{ listStyle: 'none', marginRight: '-10px', }} key={reply.id} data-testid={reply.id} variants={item} whileHover={{ // scale: 1.2, marginRight: '5px', transition: { ease: 'easeOut' }, }} > <div style={{ background: 'linear-gradient(90deg,#ffa0ae 0%,#aacaef 75%)', height: '50px', width: '50px', borderRadius: '50%', border: '3px solid #4C79DF', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', fontSize: '38px', }} > <span role="img" style={{ paddingRight: 0 }}> {reply.photo} </span> </div> </motion.li> ))} </motion.ul> </> ); }; export default Example;
Conclusion
Wow, we just learned a lot of stuff about Framer Motion! We went from building very basic animations like translations to orchestrate more complex ones involving multiple components and also tie together multiple transitions using useMotionValue
and useTransform
. You have now learned pretty much everything I know about Framer Motion and can start sprinkling some amazing animations in your own frontend work.
This is my first time trying out this format involving interactive widgets and playgrounds to illustrate what I've learned, let me know what you think! Would you like to see more articles like this one? How would you improve the widgets and examples? I'm always looking to push this blog forward and would love to get some feedback.
Did you come up with some cool animations after going through this guide?
Don't hesitate to send me a message showcasing your creations!
Want to see more?
Here are some other Framer Motion related articles or examples I came up with:
Liked this article? Share it with a friend on Bluesky or Twitter or support me to take on more ambitious projects to write about. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.
Have a wonderful day.
– Maxime
An interactive guide introducing everything I've learned about Framer Motion through fun examples and little case studies of animations I built.