Angad Sethi
View RSS feedLinkedIn

Powerful layout animations with Framer motion

Published on

Framer motion is a powerful animation library. It simplifies a lot of complex animations and really enables us to deliver extremely delightful and purposeful user interfaces and experiences. However, I've found it's real value lies in making layout animations extremely easy to implement.

A layout animation is an instance where a component's layout changes between two states and this state change is animated.

Let's illustrate this with a real-world example. Imagine a scenario where a user can click a "reveal" button to see how many votes their teammates have assigned to a story. Our goal is to create the following effect: once the user clicks "reveal," we want the cards to rearrange in ascending order of votes after a 1-second delay.

Below I've implemented this feature. Feel free to click around and understand how this works.

import React, { useState, useEffect, useId } from "react";
import "./RevealVotesAnimation.css";
import { EstimateVote } from "./EstimateVote";

const sorted = (votes) => {
  return [].slice.call(votes).sort(function (a, b) {
    return a.vote - b.vote;
  });
};

const initialVotes = [
  { vote: 4, userId: "Bohdan" },
  { vote: 2, userId: "Kaitlyn" },
  { vote: 7, userId: "Sam" },
  { vote: 3, userId: "Alfie" },
  { vote: 1, userId: "Angad" },
];

export default function App() {
  const [votes, setVotes] = useState(initialVotes);
  const [showVotes, setShowVotes] = useState(false);
  const [isSorted, setIsSorted] = useState(false);
  const id = useId();

  useEffect(() => {
    if (showVotes && !isSorted) {
      setTimeout(() => sortFunc(), 1000);
    }
  });

  const sortFunc = () => {
    const sortedVotes = sorted(votes);

    setIsSorted(true);
    setVotes(sortedVotes);
    return;
  };

  return (
    <div className={"background"}>
      <div className={"votingArea"}>
        <div className={"wrapper"}>
            {votes.map((vote) => {
              const layoutId = vote.userId;
              return (
                <EstimateVote
                  estimate={vote.vote}
                  userId={vote.userId}
                  showVote={showVotes}
                  key={layoutId}
                />
              );
            })}
        </div>

        <div className={"bottomRowWrapper"}>
          {!showVotes && (
            <button
              onClick={() => {
                setShowVotes((prev) => !prev);
              }}
            >
              Reveal votes
            </button>
          )}
          {showVotes && (
            <button
              onClick={() => {
                setShowVotes((prev) => !prev);
                setIsSorted((prev) => !prev);
                setVotes(initialVotes);
              }}
            >
              hide votes
            </button>
          )}
        </div>
      </div>
    </div>
  );
  };

I think it is safe to say that the above implementation looks janky/glitchy/snafu.

Wouldn't it be nice if we could somehow re-arrange all the votes and users in a smooth fashion so that the user understands that we mean to re-arrange the votes in ascending order? Maybe something like this...

Bohdan
Kaitlyn
Sam
Alfie
Angad

Let's animate this with framer motion's layout animations. (It actually blows my mind how easy it is add an animation to this):

  • Mark the div we want to animate as a motion component and set the attribute layout as true on it. In this case we mark the div wrapping the vote object's userId and estimate in EstimateVote.js.
return (
<motion.div layout>
{userId}
<div className={"estimateShown"}>{estimate}</div>
</motion.div>
);
import React from "react";
import "./RevealVotesAnimation.css";
import { motion } from "framer-motion";

export const EstimateVote = ({ estimate, userId, showVote }) => {
  if (!showVote && estimate) {
    return (
      <div>
        {userId}
        <div className={"hiddenCard"}></div>
      </div>
    );
  }

      return (
        <motion.div
          layout
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          transition={{ duration: 0.7,
          // Uncomment these lines below and click reveal for a funny take on the animation!
          // type: "spring",
          // stiffness: 260,
          // damping: 9 
          }}
        >
          {userId}
          <div className={"estimateShown"}>{estimate}</div>
        </motion.div>
      );
};

Note: : You'll notice that I've added some more properties on the motion component. I would highly encourage you to change these values and see the changes in animation!

Framer motion offers an unreal amount of control on the animations themselves. I was playing around with the spring animation controls and it's actually ridiculous how accurate the spring physics of it gets. Check out the framer docs here to see what's possible!

Phew, okay I don't know about you but I think that it looks pretty 🔥. Especially when compared to our initial version.

How does it work?

Framer motion uses a technique called FLIP under the hood. FLIP stands for:

  • First - initial state of animation.
  • Last - final state of animation.
  • Invert - reversing or inverting the animation path.
  • Play - simulation step where the animation is advanced over time. return

I've found this article does an incredible job at explaining each of the steps with animations! (A bit meta I know).

Why not just use CSS for layout animations?

The reason is two-fold:

  1. CSS animations that involve layout changes are typically more resource-intensive than transform-based animations, which may result in less smooth performance on lower-end devices. Framer motion uses transforms to animate everything which is quite a lot performant!
  2. Some properties like justify-content are not animatable using just CSS, but framer uses the transform method to make this possible!

You can read further on this topic here.

Will it hike up my bundle size?

Framer motion exports many functions, so the package size may look large on Bundlephobia (~50kb). However Javascript bundlers like Webpack and Rollup tree shake the unused functions and only the code imported is shipped to consumers which greatly reduces bundle size (as little as 1kb if a small hook is imported). The motion component is the core API. Because of its declarative, props-driven API, it's impossible for bundlers to tree shake it any smaller than 29kb. In lieu of this, framer-motion exports a slimmer variation of motion called m which doesn't come preloaded with features like animations, layout animations, or the drag gesture. It works the exact same as the motion component and loads in the features later using the LazyMotion component. This reduces the bundle size to just under 4.6kb for the initial render which is great! Read more here.

Accessibility

Operating systems provide a setting called "Reduce motion" that users can toggle, in order to disable animations. This control is in place since animations can potentially trigger physical symptoms like vertigo, nausea, and malaise for some. It is important to respect this setting and disable our animations if necessary. Framer motion exports a component called MotionConfig which we can use to disable animations if users have toggled reduce motion setting on.

import React from "react";
import { MotionConfig } from "framer-motion";

function App() {
return (
<MotionConfig reducedMotion="user">
{/* The entire application here */}
</MotionConfig>
);
}
export default App;

Framer motion also exports a hook called useReducedMotion which returns true if the user has this setting toggled on. More info here.

Wrapping up

I hope I've been successful in detailing just how easy it is to enable layout animations in our apps by leveraging Framer-motion and how powerful we are able to make these animations. Hopefully the flashes in your apps will now catch your eye and you'll be well equipped to eliminate them!