Lesson 7/9

Make the animation loop with keyframes

This free lesson is part of the Framer Motion course.

A lifetime subscription (€149 once) to Frontend.FYI PRO gives you instant access to all content.

Just like CSS animations, you can use keyframes to make multi step animations in Framer Motion too.

In order to create a keyframe animation, all you have to do is add multiple values as an array to a single property: scale: [1, 2, 2, 1, 1].

In the playground below you find an example straight from the Framer Motion docs, where they showcase how to combine these keyframes and custom animation settings, to create a nice looking animation.

import { motion } from "framer-motion";

const App = () => {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <motion.div
        className="bg-gray-400 w-24 h-24"
        animate={{
          scale: [1, 2, 2, 1, 1],
          rotate: [0, 0, 180, 180, 0],
          borderRadius: ["0%", "0%", "50%", "50%", "0%"],
        }}
        transition={{
          duration: 2,
          ease: "easeInOut",
          repeat: Infinity,
          repeatDelay: 1
        }}
      />
    </div>
  );
};

export default App;

Notice they added multiple properties for the animations of scale, rotate and borderRadius. Then they use the duration, repeat and repeatDelay properties we discussed in the previous chapter to make the animation loop.

Try removing the repeat and repeatDelay and notice the animation only plays once.

Animation steps are spread out evenly

Framer Motion spreads out the animations evenly over the duration of the animation. This means, if you have 5 values in your keyframe animation, and you set the duration to 5 seconds, the animation from one step to the next will take 1 second.

This also means if you make the scale animation have 3 steps and the rotation would still have 5, the animation of the scale takes longer than the rotation, and animations start to overlap each other.

This is not a bad thing, but can make it a bit harder to get animation properties to line up.

Sounds confusing? Let’s take a look at the following example. The light gray box only has 3 scale properties, where the dark gray box has the same 5 steps we use in the previous example.

import { motion } from "framer-motion";

  const App = () => {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <motion.div
          className="bg-gray-400 w-24 h-24"
          animate={{
            scale: [1, 2, 1],
            rotate: [0, 0, 180, 180, 0],
            borderRadius: ["0%", "0%", "50%", "50%", "0%"],
          }}
          transition={{
            duration: 2,
            ease: "easeInOut",
            repeat: Infinity,
            repeatDelay: 1
          }}
        />
        <motion.div
          className="bg-gray-800 w-24 h-24"
          animate={{
            scale: [1, 2, 2, 1, 1],
            rotate: [0, 0, 180, 180, 0],
            borderRadius: ["0%", "0%", "50%", "50%", "0%"],
          }}
          transition={{
            duration: 2,
            ease: "easeInOut",
            repeat: Infinity,
            repeatDelay: 1
          }}
        />
      </div>
    );
  };

  export default App;

The rotation and border radius happen at exactly the same time, but the scale animations are not aligned.

The times property

By default Framer Motion will spread the animation steps out evenly over the duration of the animation. However, of course you can have more control over this. You can change this behaviour by adding a times property to the transition.

Each property of the times array has to be a number between 0 and 1, representing the percentage of the animation that has passed. So if you have 5 steps, the values would be automatically set at [0, 0.25, 0.5, 0.75, 1] which corresponds to 0%, 25%, 50%, 75% and 100% of the animation.

You could however also set the times property to [0, 0.5, 0.75, 0.875, 1] to make the first step take up 50% of the animation.

import { motion } from "framer-motion";

  const App = () => {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <motion.div
          className="bg-gray-400 w-24 h-24"
          animate={{
            scale: [1, 2, 2, 1, 1],
            rotate: [0, 0, 180, 180, 0],
            borderRadius: ["0%", "0%", "50%", "50%", "0%"],
          }}
          transition={{
            duration: 2,
            ease: "easeInOut",
            repeat: Infinity,
            repeatDelay: 1,
            times: [0, 0.5, 0.75, 0.875, 1]
          }}
        />
        <motion.div
          className="bg-black w-24 h-24"
          animate={{
            scale: [1, 2, 2, 1, 1],
            rotate: [0, 0, 180, 180, 0],
            borderRadius: ["0%", "0%", "50%", "50%", "0%"],
          }}
          transition={{
            duration: 2,
            ease: "easeInOut",
            repeat: Infinity,
            repeatDelay: 1
          }}
        />
      </div>
    );
  };

  export default App;

Automatically infer the initial value

One final cool thing about Framer’s keyframes is you can set the intial frame as null, and Framer will automatically infer the initial value from the current value of the element. This removes the need to duplicate these styles in both your CSS and your Framer Motion code.

This is similar to skipping the initial prop we discussed in lesson 2.5.

<motion.div
// rounded-full will set the border radius in CSS
className="bg-black w-24 h-24 rounded-full"
animate={{
scale: [1, 2, 2, 1, 1],
rotate: [0, 0, 180, 180, 0],
// We then infer this as a default by using null
borderRadius: [null, "0%", "50%", "50%", "0%"],
}}
/>