Glyn Lewington Web developer with a focus on the frontend, particularly React. I also like writing technical blog posts to help others with difficult topics I encounter.

Another React animation library? Here’s why you should use Framer Motion

11 min read 3165

Another React Animation Library? Here's Why You Should Use Framer Motion

If you’re like me, your first thought when you read this headline might be, “Why do we need yet another animation library for React? This is getting tiring!”

Think of Framer Motion as more of an improvement or reinvention of an existing animation library than a brand new one.

Framer Motion is the successor to Pose, which was one of the most popular animation libraries used with React. Like Pose, it’s built upon promotion, which is a low-level, unopinionated animation library, but it provides abstractions to streamline the process.

Framer Motion improves upon and simplifies the API in a way that couldn’t have been done without breaking changes and rewriting. One difference is that whereas Framer Motion only has support for React, Pose has support for React-Native and Vue. If you’re currently using Pose I would recommend updating to Framer Motion because Pose has been depreciated.

What are spring animations?

Framer Motion uses spring animations by default. You’ll need to customize these if you want different behavior. Spring animations have gained tremendous popularity in recent years. The alternative is easing animations, which you create with CSS, e.g., 1s ease-in. Easing animations simply don’t look natural or realistic with their behavior and set duration.

Spring animations apply the laws of physics to have smoother, more natural animations. They do this through physics principals such as momentum. They don’t simply reach and stop at the end state; they sort of bounce past and settle into place. Two values are used to define a spring animation: stiffness and damping. Tweaking these values will make the animation behave differently.

Why use Framer Motion?

If most animation libraries use spring-based animations, then, why should you use Framer Motion? For starters, it has a great API that is simple and doesn’t fill your components with extra code. In most cases, you can simply replace your HTML element with a motion element — for example, div with motion.div, which results in the same markup but has additional props for animation.

The documentation and examples on Framer Motion’s official website are the best I’ve seen in an animation library. For most common use cases, you can find a CodeSandbox example in the documentation. Some are simple, some more complex, but they all give you an excellent understanding and enable you to tweak to build your own solution. Framer Motion can also handle SVG animations, unlike most libraries.

Lastly, since it is part of Framer and integrates with the Framer X design tool, using both tools can help make your workflow smoother and more efficient (I don’t have any experience with this personally, but I imagine they are great together).

How it works

As mentioned, Framer Motion replaces HTML elements with motion elements. There is a motion element for every HTML and SVG element (e.g., <motion.div>). These motion elements hook into Framer Motion and accept additional props which define animation behavior. The simplest of these props is animate and is also the one you will be reaching for most often. Any valid value you pass to animate will cause the component to animate to that state upon mount.

We made a custom demo for .
No really. Click here to check it out.

<motion.div 
  animate={{
    x: '100px'    
  }}
>
Weeee I'm animated
</motion.div>

This will cause the div to slide 100 pixels to the right when loaded. If you want it to animate through a series of states, you can use keyframes, which are an array of state values.

<motion.div 
  animate={{
    x: ['100px', '0px', '100px']  
  }}
>
Weeee I'm animated
</motion.div>

This will slide it 100 pixels to the right, back to its original position, and then 100 pixels back to the right. By default, these all take equal amounts of time, but you can tweak that setting using the times prop if desired.

By default, an element will animate from where it is naturally styled to the state defined in animation. In some cases, you will want to define the state at which the animation starts. Take opacity, for example. Let’s say you want it to start not from 0 but rather a visible value such as 0.5. This can be defined by passing the state to the initial prop. If you pass a value of false, the initial state will be the value in animation and no initial animation will occur.

<motion.div
  initial={{
    opacity: 0.5
  }}
  animation={{
    opacity: 1
  }}
>
</motion.div>

Although I mentioned that Framer Motion is a spring-based library, that’s not technically 100 percent true. For animations that don’t involve movement, springs aren’t possible. These use tween animations, such as for opacity and color. In addition, inertia is calculated on the initial velocity and used for dragTransition.

Animations can be changed to use a different type or tweaked in other ways via the transition prop. If, for example, you want to set the duration to 2 and use the tween style, you would pass the following.

<motion.div 
  animate={{
    x: '100px'    
  }}
  transition={{
    type: 'tween',
    duration: 2
  }}
>
Weeee I'm animated
</motion.div>

There are other powerful transitions available, such as stagger and delay children. These allow you to easily perform what have traditionally been complex animations involving multiple child elements.

Often you want an animation to fire when an element is clicked, hovered over, or otherwise interacted with. Thankfully, Framer Motion has built-in props called gestures.

The gestures available are hover, tap, pan, and drag. hover and tap are prefixed with while and, as you might expect, apply the animation only while hovered or tapped. When the user is no longer hovering or tapping, it will revert back to the default style with a smooth animation. drag and pan work differently. Enabling dragging allows you to drag the element anywhere on the page and it will remain in the location it is dragged to. The dragging feels very natural with the default physics; dragging builds up momentum and, when released, the element continues in that direction depending on how hard and fast you’re dragging.

These are what I consider the foundational building blocks of Framer Motion. With these techniques, you can build almost any type of animation you could think of.

More advanced techniques are necessary to do things such as animate the unmounting of components, but we’ll cover some of these later with an example.

Framer Motion in action

That’s enough about the library itself. Now let’s build a practical model to showcase common techniques and help us see this library in action.

Starting from a basic React application (such as Create React App), we need to install Framer Motion.

npm install framer-motion

Our basic application will look like this, and we’ll write our application in App.

// index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "./styles.css";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root"),
);

styles.css has just a few rules for some basic styles to work with.

// styles.css
html,
body,
#root {
  height: 100%;
  width: 100%;
}

.app {
  height: 100%;
  width: 100%;
}

.list {
  display: flex;
  flex-wrap: wrap;
  width: 1200px;
}

img {
  // disables dragging on img elements
  pointer-events: none;
}

We’ll also use Bootstrap to shortcut the styling. Add the following link to the head of your HTML to include it.

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
crossorigin="anonymous"
/>

Here’s our starting App.js. We’ll have a list of items/cards where additional items can be added or existing ones removed. They’ll span multiple rows and columns and you’ll be able to reorder them by dragging. I’ve included random images from Unsplash to make it more interesting.

import React, { useState } from 'react';
import { motion } from 'framer-motion';

const App = () => {
  const [cards, setCards] = useState(defaultCards);

  async function addCard() {
    const res = await fetch('https://source.unsplash.com/random');
    if (!res.ok) return;
    const id = cards.length === 0 ? 0 : cards[cards.length - 1].id + 1;
    const card = { id, img: res.url };
    setCards([...cards, card]);
  }

 function removeCard() {
    setCards(cards.filter((card) => card !== card.id));
  }

  return (
    <div className='app'>
      <div>
        <h1>Sweet Animations!</h1>
      </div>
      <button className="btn btn-primary" onClick={addCard}>
        Add Card
      </button
      <div className='list'>
        {cards.map(card => (
          <Card
            card={card}
            setCards={setCards}
            removeCard={removeCard}
            key={card.id} />
        ))}
      </div>
    </div>  
  )
}

const Card = ({ card, setCards, removeCard }) => {
  function handleRemove() {
    removeCard(card.id);
  }

  return (
    <div className="card" style={{ width: "18rem" }}>
      <img src={card.img} className="card-img-top" alt="..." />
      <div className="card-body">
        <h5 className="card-title">Cool Image</h5>
      </div>
    </div>
  );
}

const defaultCards = [
  {
    id: 0,
    img: "https://images.unsplash.com/photo-1587900437942-8758241767ef?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixlib=rb-1.2.1&q=80&w=300",
  },
  {
    id: 1,
    img: "https://images.unsplash.com/photo-1586336900429-71f0642f66fd?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixlib=rb-1.2.1&q=80&w=300",
  },
];

Let’s start by adding an animation to play when a new image is added. First, we’ll need to convert our Card component to use a motion.div rather than div so that we have access to all the animations.

const Card = ({ card, removeCard }) => {
  function handleRemove() {
    removeCard(card.id);
  }

  return (
    <motion.div className="card" style={{ width: "18rem" }}>
      <img src={card.img} className="card-img-top" alt="..." />
      <div className="card-body">
        <h5 className="card-title">Cool Image</h5>
      </div>
    </motion.div>
  );
};

We’ll then need to add both an initial value and an animate value to our motion.div. We’ll make it slide and fade in from the left.

initial={{ x: "-300px", opacity: 0 }}
animate={{ x: 0, opacity: 1 }

SVG animation

Animating SVGs is one of my favorite features of React Motion. We’ll add a remove button which will be a motion.svg with motion.paths. You can animate the pathLength, pathSpacing, and pathOffset properties of a path element.

<motion.div className="card" style={{ width: "18rem" }}>
      <motion.svg
        xmlns="http://www.w3.org/2000/svg"
        width="24"
        height="24"
        viewBox="0 0 24 24"
        fill="none"
        stroke="#333"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        onClick={handleRemove}
      >
        <motion.path d="M 18 6 L 6 18"  />
        <motion.path d="M 6 6 L 18 18"  />
      </motion.svg>      
      <img src={card.img} className="card-img-top" alt="..." />
      <div className="card-body">
        <h5 className="card-title">Cool Image</h5>
      </div>
    </motion.div>

Let’s add a couple of animations to this SVG. On mount, it will draw the two lines one after another. On hover, it will thicken the lines.

To achieve this effect, we need the parent motion.svg and child motion.paths to be linked so we can stagger the animations of the child paths.

We can do this using a Framer Motion prop called variants. variants accepts an object with properties of defined states. You can then pass these as the value of animation states, such as animate or initial. These values will automatically be passed down to children unless overridden.

// example usage
<motion.div
  variants={{ 
    mount: { opacity: 0.5 },
    rest: { opacity: 1 }
  }}
  initial='mount'
  animate='rest'
/>

We’ll focus on three states: initial, animate, and hover. Within animate, we also need to define a transition, which will determine how long the animation takes and the staggering of children.

staggerChildren defines how much time should pass between each child’s animation starting. when defines the point at which these animations should begin. A value of afterChildren means that the stagger delay will occur after the child animation. beforeChildren means the opposite — waiting the defined time before the animation.

The major difference here is on the first animation, whether it starts immediately (afterChildren) or waits briefly (beforeChildren). We don’t want to delay the first child’s animation, so we’ll use afterChildren.

const variants = {
  initial: {
    strokeWidth: 2,
    pathLength: 0,
  },
  animate: {
    pathLength: 1,
    transition: { duration: 1, when: "afterChildren", staggerChildren: 1 },
  },
  hover: {
    strokeWidth: 4,
  },
};


<motion.svg
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="#333"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
  variants={variants}
  initial="initial"
  animate="animate"
  whileHover="hover"
  onClick={handleRemove}
>
  <motion.path d="M 18 6 L 6 18" variants={variants} />
  <motion.path d="M 6 6 L 18 18" variants={variants} />
</motion.svg>

Similar to the animation we created on mount, we want to create one for the exit animation, which will slide up and fade out.

When a Card is removed, React removes the component from the DOM. Because of this, we need to wrap our Card‘s components with a special component, AnimatePresence. This allows Framer Motion to track when the component is unmounting and delay it until the animation is finished.

We can actually wrap the list of components rather than wrapping each individually, but we need to make sure they are the direct children.

<div className="list">
    <AnimatePresence>
      {cards.map((card) => (
        <Card key={card.id} card={card} removeCard={removeCard} />
      ))}
    </AnimatePresence>
</div>

Once wrapped by this component, the motion.div accepts an exit prop, which will be the state animated to on exit.

<motion.div
  initial={{ x: "-300px", opacity: 0 }}
  animate={{ x: 0, opacity: 1 }}
  exit={{ y: '-300px', opacity: 0 }}
>
</motion.div>

When a Card is removed, let’s say we want the other Cards to slide across rather than jump into position. Framer Motion makes this typically difficult task a breeze. A motion component accepts a prop positionTransition, which will automatically animate any changes to its position in the layout.

 <motion.div
  initial={{ x: "-300px", opacity: 0 }}
  animate={{ x: 0, opacity: 1 }
  exit={{ exit={{ y: '-300px', opacity: 0 }}
  positionTransition
>
</motion.div>

One thing you may notice is that, unfortunately, the animations to the new positions won’t occur until the removed items animation is complete. The item still exists in the DOM until the animation is complete and, therefore, the position of the other items has yet to change. This is not an easy issue to solve, and the author of Framer Motion has acknowledged it as a difficult case.

They’re currently working on a better solution that will hopefully come in a future release. For now, you can get around this by adding some conditional styling to the items, which will remove them from the layout of the DOM while the exit animation is occurring. This allows the position transitions to occur simultaneously.

To achieve this, we’ll need to add some state to our Card component. We need to track whether it’s currently being removed, and we need to track it’s position at the time of removal so that we can position it in that same location but remove it from the layout.

We’ll get its location using a ref, which is React’s method for accessing DOM nodes, and getBoundingClientRect. When removing an item, it will first update isRemoving and position. While being removed, we’ll style the Card with position: absolute, removing it from the layout and setting left and top to the calculated positions.

const Card = ({ card, removeCard }) => {
  const [isRemoving, setIsRemoving] = useState(false);
  const [position, setPosition ] = useState({ left: 0, top: 0})
  const cardRef = useRef(null);

  function handleRemove() {
    const position = cardRef.current.getBoundingClientRect();
    setPosition(position);
    setIsRemoving(true);  
    removeCard(card.id);    
  }
  return (
    <motion.div
      initial={{ x: "-300px", opacity: 0 }}
      animate={{ x: 0, opacity: 1 }}
      exit={{ y: "-300px", opacity: 0 }}
      positionTransition
      className="card"
      style={{
        width: "18rem",
        position: isRemoving ? "absolute" : "block",
        left: isRemoving ? position.left : "auto",
        top: isRemoving ? position.top : "auto",
      }}
      ref={cardRef}
    >
      <img src={card.img} className="card-img-top" alt="..." />
      // ... svg
      <div className="card-body">
        <h5 className="card-title">Cool Image</h5>
      </div>
    </motion.div>
  );
}

Dragging

Another common feature is the ability to reorder a list of items by dragging them. We’ll enable a drag to move a card to the left/right or up/down. First, we need to enable dragging with the drag prop. We’ll allow dragging in any direction, but if you had a vertical/horizontal-only list, you could constrain the direction with drag='x' or drag='y'.

Note: Since we’re using an image, we’ll also have to disable the natural drag on an *img* element. Depending on your browser support, you can do this a few ways. Here we kept it simple with some CSS to remove pointer events, which is already done in our CSS file.

<motion.div
  // other properties
  drag
/>

This will allow you to drag the element anywhere on the page, but it will actually stay in that position. You can define the to which area you want an element to be draggablewith dragConstraints.

dragConstraints takes an object with top/bottom/left/right properties. We’ll set these all to 0 so the element returns to the same position after dragging (you can still somewhat drag it beyond these bounds). If you want to tweak how far the element can drag from these constraints, you can use the dragElastic prop, which takes a value between 0 (no dragging beyond the constraints) and 1 (drag as far as you desire from the constraints).

<motion.div
  // other properties
  drag
  dragConstraints={{ top: 0, right: 0, bottom: 0, left: 0 }} 
/>

There are also a few callback function props you can use to plug in to different behavior — namely, onDrag, onDragStart, onDragEnd. These pass arguments of the event and a drag object with relevant information. We’ll use onDragEnd to calculate our repositioning.

Note: You can create really nice list reordering with *onDrag*, but it requires a lot of logic regarding positioning and calculation, which we don’t want to get into here. Framer Motion provides a great example if you want to build something like this — although at that point I would probably recommend reaching for a library like *react-beautiful-dnd*.

<motion.div
  // other properties
  drag
  dragConstraints={{ top: 0, right: 0, bottom: 0, left: 0 }}
  onDragEnd={(e, drag) => moveCard(drag.point, i)} 
  // we will write the move card function in a moment
/>

With the drag argument passed to onDragEnd, we can get the distance the element is dragged from its natural position. In moveCard, we’ll use this point to determine the direction and whether it was dragged far enough to constitute a reorder.

// App.js
function moveCard(point, index) {
  let reorderedCards = [...cards];
  let movedFrom = index;
  let movedTo = index;

  if (point.x <= -100) {
    movedTo = index - 1;
  }
  if (point.x >= 100) {
    movedTo = index + 1;
  }
  if (point.y >= 100) {
    movedTo = index + 4;
  }
  if (point.y <= -100) {
    movedTo = index - 4;
  }

  if (movedFrom !== movedTo && cards[movedFrom] && cards[movedTo]) {
    reorderedCards[movedFrom] = cards[movedTo];
    reorderedCards[movedTo] = cards[movedFrom];
    setCards(reorderedCards);
  }
}

// don't forget to pass it down as a prop to the `Card` element.

Summary

Now we have a basic application that implements a variety of common features and animations. Hopefully, this tutorial gives you a solid foundation to add some great animations to your library and build upon them over time to suit all your needs.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Glyn Lewington Web developer with a focus on the frontend, particularly React. I also like writing technical blog posts to help others with difficult topics I encounter.

Leave a Reply