@MaximeHeckel

Guide to creating animations that spark joy with Framer Motion

December 15, 2020 / 15 min read

Last Updated: January 31, 2023

Over 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:

  1. ArrowAn icon representing an arrow
    "Where/how is my element at the beginning?" i.e the initial state
  2. ArrowAn icon representing an arrow
    "Where it needs to go or which shape it needs to take by the end?" i.e. the target state
  3. ArrowAn icon representing an arrow
    "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:

  • ArrowAn icon representing an arrow
    initial: the state of our element at mount time.
1
<motion.div
2
...
3
initial={{
4
x: 0,
5
rotate: 45,
6
}}
7
...
8
/>
  • ArrowAn icon representing an arrow
    animate: the state in which our element will be at the end of the animation.
1
<motion.div
2
...
3
animate={{
4
x: 50,
5
rotate: 270,
6
}}
7
...
8
/>
  • ArrowAn icon representing an arrow
    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.div
2
...
3
transition={{
4
ease: "easeIn",
5
duration: 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:

Spring
1
<motion.div
2
...
3
transition={{
4
type: 'spring',
5
stiffness: 100,
6
mass: 3,
7
damping: 1,
8
}}
9
/>
10
Tween
1
<motion.div
2
...
3
transition={{
4
type: 'tween',
5
ease: 'easeInOut',
6
duration: 2,
7
...
8
}}
9
/>
10
Inertia
1
<motion.div
2
...
3
transition={{
4
type: 'inertia',
5
velocity: 50,
6
}}
7
/>
8
9
10

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:

  • ArrowAn icon representing an arrow
    **remove the **transition prop from the first component (Example1). Notice that this translation animation went from an ease type to a spring type. This comes from the "smart defaults" we just mentioned.
  • ArrowAn icon representing an arrow
    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

1
import { motion } from 'framer-motion';
2
3
const AnimatedButton = () => {
4
const buttonVariants = {
5
hover: {
6
scale: 1.5,
7
},
8
pressed: {
9
scale: 0.5,
10
},
11
rest: {
12
scale: 1,
13
},
14
};
15
16
return (
17
<motion.button
18
initial="rest"
19
whileHover="hover"
20
whileTap="pressed"
21
variants={buttonVariants}
22
>
23
Click 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

1
import { motion } from 'framer-motion';
2
3
const AnimatedButton = () => {
4
const buttonVariants = {
5
// any variant declared as a function will inherit the `custom prop` as argument
6
hover: (clicked) => ({
7
// once clicked the button will not scale on hover anymore
8
scale: clicked ? 1 : 1.5,
9
}),
10
pressed: {
11
scale: 0.5,
12
},
13
rest: {
14
scale: 1,
15
},
16
};
17
18
const [clicked, setClicked] = React.useState(false);
19
20
return (
21
<motion.button
22
initial="rest"
23
whileHover="hover"
24
whileTap="pressed"
25
variants={buttonVariants}
26
custom={clicked}
27
onClick={() => setClicked(true)}
28
>
29
Click 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:

  • ArrowAn icon representing an arrow
    make the first button scale on hover (for now, it only rotates).
  • ArrowAn icon representing an arrow
    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:

  • ArrowAn icon representing an arrow
    we know the main elements that define an animation ✅
  • ArrowAn icon representing an arrow
    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:

1
f: y -> x * 2
2
3
// Where x is the pathLength of our SVG y is the opacity

which is equivalent to the following code using Framer Motion's hooks:

1
const pathLength = useMotionValue(0);
2
const 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

1
import React from 'react';
2
import { motion, useMotionValue, useTransform } from 'framer-motion';
3
4
const CopyToClipboardButton = () => {
5
const duration = 0.4;
6
7
const clipboardIconVariants = {
8
clicked: { opacity: 0 },
9
unclicked: { opacity: 1 },
10
};
11
12
const checkmarkIconVariants = {
13
clicked: { pathLength: 1 },
14
unclicked: { pathLength: 0 },
15
};
16
17
const [isClicked, setIsClicked] = React.useState(false);
18
19
const pathLength = useMotionValue(0);
20
const opacity = useTransform(pathLength, [0, 0.5], [0, 1]);
21
22
return (
23
<button
24
css={{
25
background: 'transparent',
26
border: 'none',
27
cursor: isClicked ? 'default' : 'pointer',
28
outline: 'none',
29
marginBottom: '20px',
30
}}
31
aria-label="Copy to clipboard"
32
title="Copy to clipboard"
33
disabled={isClicked}
34
onClick={() => {
35
setIsClicked(true);
36
}}
37
>
38
<svg
39
width="100"
40
height="100"
41
viewBox="0 0 25 25"
42
fill="none"
43
xmlns="http://www.w3.org/2000/svg"
44
>
45
<motion.path
46
d="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"
47
stroke="#949699"
48
strokeWidth="2"
49
strokeLinecap="round"
50
strokeLinejoin="round"
51
initial={false}
52
animate={isClicked ? 'clicked' : 'unclicked'}
53
variants={clipboardIconVariants}
54
transition={{ duration }}
55
/>
56
<motion.path
57
d="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"
58
stroke="#949699"
59
strokeWidth="2"
60
strokeLinecap="round"
61
strokeLinejoin="round"
62
initial={false}
63
animate={isClicked ? 'clicked' : 'unclicked'}
64
variants={clipboardIconVariants}
65
transition={{ duration }}
66
/>
67
<motion.path
68
d="M20 6L9 17L4 12"
69
stroke="#5184f9"
70
strokeWidth="2"
71
strokeLinecap="round"
72
strokeLinejoin="round"
73
initial={false}
74
animate={isClicked ? 'clicked' : 'unclicked'}
75
variants={checkmarkIconVariants}
76
style={{ pathLength, opacity }}
77
transition={{ 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:

  • ArrowAn icon representing an arrow
    variants for the clipboard SVG and the checkmark SVG
1
const clipboardIconVariants = {
2
clicked: { opacity: 0 },
3
unclicked: { opacity: 1 },
4
};
5
6
const checkmarkIconVariants = {
7
clicked: { pathLength: 1 },
8
unclicked: { pathLength: 0 },
9
};
  • ArrowAn icon representing an arrow
    useMotionValue and useTransform to intertwine the opacity and pathLength values together
1
const pathLength = useMotionValue(0);
2
const 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:

  • ArrowAn icon representing an arrow
    Delays and repetitions: "move to point A, then 2 seconds later move to point B then repeat"
  • ArrowAn icon representing an arrow
    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:

  • ArrowAn icon representing an arrow
    you can try to change the repeat type from mirror to loop and observe the subtle change of repetition type.
  • ArrowAn icon representing an arrow
    make the animation repeat indefinitely instead of just 3 times.
  • ArrowAn icon representing an arrow
    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

1
const boxVariants = {
2
out: {
3
y: 600,
4
},
5
in: {
6
y: 0,
7
transition: {
8
duration: 0.6,
9
// Both children will appear 1.2s AFTER the parent has appeared
10
delayChildren: 1.2,
11
},
12
},
13
};
14
15
const iconVariants = {
16
out: {
17
x: -600,
18
},
19
in: {
20
x: 0,
21
},
22
};
23
24
return (
25
<motion.div variants={boxVariants} initial="out" animate="in">
26
<motion.span
27
role="img"
28
aria-labelledby="magic wand"
29
variants={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

1
const boxVariants = {
2
out: {
3
y: 600,
4
},
5
in: {
6
y: 0,
7
transition: {
8
duration: 0.6,
9
// The first child will appear AFTER the parrent has appeared on the screen
10
delayChildren: 1.2,
11
// The next sibling will appear 0.5s after the previous one
12
staggerChildren: 0.5,
13
},
14
},
15
};
16
17
const iconVariants = {
18
out: {
19
x: -600,
20
},
21
in: {
22
x: 0,
23
},
24
};
25
26
return (
27
<motion.div variants={boxVariants} initial="out" animate="in">
28
<motion.span
29
role="img"
30
aria-labelledby="magic wand"
31
variants={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.