Categories
React

Framer Motion — Shared Layout Animations

Spread the love

With the Framer Motion library, we can render animations in our React app easily.

In this article, we’ll take a look at how to get started with Framer Motion.

Shared Layout Animations

We can create shared layout animations with the AnimateSharedLayout component.

For example, we can write:

App.js

import React, { useState } from "react";
import { AnimatePresence, AnimateSharedLayout, motion } from "framer-motion";
import "./styles.css";

function Item({ text }) {
  const [isOpen, setIsOpen] = useState(false);

const toggleOpen = () => setIsOpen(!isOpen);

return (
    <motion.li layout onClick={toggleOpen} initial={{ borderRadius: 10 }}>
      <motion.div layout>{text}</motion.div>
      <AnimatePresence>{isOpen && <Content />}</AnimatePresence>
    </motion.li>
  );
}

function Content() {
  return (
    <motion.div
      layout
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      <div className="row" />
      <div className="row" />
      <div className="row" />
    </motion.div>
  );
}

const items = [
  { text: "foo", id: 1 },
  { text: "bar", id: 2 },
  { text: "baz", id: 3 }
];

export default function App() {
  return (
    <AnimateSharedLayout>
      <motion.ul layout>
        {items.map(({ text, id }) => (
          <Item text={text} key={id} />
        ))}
      </motion.ul>
    </AnimateSharedLayout>
  );
}

styles.css

html,
body {
  min-height: 100vh;
  padding: 0;
  margin: 0;
}

* {
  box-sizing: border-box;
}

body {
  background-repeat: no-repeat;
  display: flex;
  justify-content: center;
  align-items: center;
}

.App {
  font-family: sans-serif;
  text-align: center;
}

ul,
li {
  list-style: none;
  margin: 0;
  padding: 0;
}

ul {
  width: 300px;
  display: flex;
  flex-direction: column;
  background: white;
  padding: 20px;
  border-radius: 25px;
}

li {
  background-color: rgba(214, 214, 214, 0.5);
  border-radius: 10px;
  padding: 20px;
  margin-bottom: 20px;
  overflow: hidden;
  cursor: pointer;
}

li:last-child {
  margin-bottom: 0px;
}

.avatar {
  width: 40px;
  height: 40px;
  background-color: #666;
  border-radius: 20px;
}

.row {
  width: 100%;
  height: 8px;
  background-color: #999;
  border-radius: 10px;
  margin-top: 12px;
}

We have the Item component that lets us toggle the content of the div.

This is done with the AnimatPresence component which shows the Content component’s content if it’s true .

The row class creates the bars that we want to show.

In App , we have the AnimateSharedLayout component wrapped around our ul to let us show the same animation for each li .

The animation is synced across a set of components that don’t share state.

And they let us perform animation between different components with a common layoutId as they’re added or removed.

When a component with the 1ayoutId prop is removed from one part of the tree and it’s added elsewhere, the new component will automatically animate from the old component’s position.

For example, we can write:

App.js

import React, { useState } from "react";
import { AnimateSharedLayout, motion } from "framer-motion";
import "./styles.css";

export default function App() {
  const [selected, setSelected] = useState(colors[0]);

return (
    <AnimateSharedLayout>
      <ul>
        {colors.map((color) => (
          <Item
            key={color}
            color={color}
            isSelected={selected === color}
            onClick={() => setSelected(color)}
          />
        ))}
      </ul>
    </AnimateSharedLayout>
  );
}

function Item({ color, isSelected, onClick }) {
  return (
    <li className="item" onClick={onClick} style={{ backgroundColor: color }}>
      {isSelected && (
        <motion.div
          layoutId="outline"
          className="outline"
          initial={false}
          animate={{ borderColor: color }}
          transition={spring}
        />
      )}
    </li>
  );
}

const colors = ["red", "green", "blue", "yellow"];

const spring = {
  type: "spring",
  stiffness: 500,
  damping: 30
};

styles.css

html,
body {
  min-height: 100vh;
  padding: 0;
  margin: 0;
}

* {
  box-sizing: border-box;
}

body {
  background-repeat: no-repeat;
  display: flex;
  justify-content: center;
  align-items: center;
}

.App {
  font-family: sans-serif;
  text-align: center;
}

ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  width: 280px;
  height: 380px;
}

.item {
  width: 100px;
  height: 50px;
  margin: 20px;
  position: relative;
  cursor: pointer;
  flex-shrink: 0;
}

.outline {
  position: absolute;
  top: -20px;
  left: -20px;
  right: -20px;
  bottom: -20px;
  border: 10px solid white;
}

We add a frame around the li that’s clicked on by toggling the isSelected state in the Item component.

In styles.css , we have the outline class to define the outline.

That is toggled when we toggle the li .

In the App component, we select the item that we clicked on with the setSelected function.

Conclusion

We can add shared layouts animation to perform animations synced across a set of components that don’t share states.

And we can also perform animations between different components with a common layoutId as they’re added or removed.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *