Framer Motion

How do I code animations

In this lesson, we’ll cover some of the advanced features of Framer Motion that I often use.

Framer Motion How do I code animations

Overview

In this lesson, we’ll cover some of the advanced features of Framer Motion that I often use.

Layout animations

The most powerful feature of this library is the layout prop. This is what enables our animations to feel native on the web. If we’d want to recreate the App Store’s card animation, we’d have to make the card we clicked on larger to cover the whole screen.

But how do we actually make that smooth transition on the web? We can’t animate it to position fixed. We’d probably need to measure the screen’s dimensions and spend a lot of time animating it.

That’s where layout animations come in. They allow us to animate any layout changes easily. Properties that were not possible to animate before, like flex-direction or justify-content, can now be animated smoothly by simply adding the layout prop.

Let’s imagine the yellow box below is the App Store card. It covers the whole screen when you click on it, but there is no transition. Try using Framer Motion to animate it. We are talking about layout animations, so you’ll probably need to use them. Here are the docs for reference.

import { motion } from "motion/react";
import { useState } from "react";

export default function Example() {
const [open, setOpen] = useState(false);

return (
  <div className="wrapper">
    <div
      onClick={() => setOpen((o) => !o)}
      className="element"
	style={
        open
          ? { position: "fixed", inset: 0, width: "100%", height: "100%" }
          : { height: 48, width: 48 }
      }
    />
  </div>
);
}

This is relatively simple to implement, but there is a lot of magic happening under the hood. Because of that, things might not always work as expected in some more complex animations that involve layout changes. You can experience some distortion, or other weird issues.

If you are interested in learning how layout animations work under the hood, you can read Inside Framer’s Magic Motion. It’s an article about reacreating Framer Motion’s layout animations from scratch.

Shared layout animations

Thanks to shared layout animations, we can basically connect two elements and create a smooth transition between them. Below is an example of that. We are not actually moving the element to the center ourselves, the library does it for us.

The Oddysey

Explore unknown galaxies.

Angry Rabbits

They are coming for you.

Ghost town

Scarry ghosts.

Pirates in the jungle

Find the treasure.

Lost in the mountains

Be careful.

We can also use it to make the highlight animation below. The highlight (div with gray color) is essentially rendered for the active element only.

  • Saved Sites
  • Collections
  • 48 Following
  • 32 Followers

This might be a bit confusing when you first see it so let’s break it down.

{TABS.map((tab) => (
  <motion.li
    layout
    className={clsx(
      "relative cursor-pointer px-2 py-1 text-sm outline-hidden transition-colors",
      activeTab === tab ? "text-gray-800" : "text-gray-700",
    )}
    tabIndex={0}
    key={tab}
    onFocus={() => setActiveTab(tab)}
    onMouseOver={() => setActiveTab(tab)}
    onMouseLeave={() => setActiveTab(tab)}
  >
    {activeTab === tab ? (
      <motion.div
        layoutId="tab-indicator"
        className="absolute inset-0 rounded-lg bg-black/5"
      />
    ) : null}
    <span className="relative text-inherit">{tab}</span>
  </motion.li>
))}
{TABS.map((tab) => (
  <motion.li
    layout
    className={clsx(
      "relative cursor-pointer px-2 py-1 text-sm outline-hidden transition-colors",
      activeTab === tab ? "text-gray-800" : "text-gray-700",
    )}
    tabIndex={0}
    key={tab}
    onFocus={() => setActiveTab(tab)}
    onMouseOver={() => setActiveTab(tab)}
    onMouseLeave={() => setActiveTab(tab)}
  >
    {activeTab === tab ? (
      <motion.div
        layoutId="tab-indicator"
        className="absolute inset-0 rounded-lg bg-black/5"
      />
    ) : null}
    <span className="relative text-inherit">{tab}</span>
  </motion.li>
))}

Here we have two rectangles. When we toggle the showSecond state value, one of them disappears, and the other one appears. Our goal is to have a smooth transition between them. Bonus points if you can get the button to transition smoothly as well, right now it just jumps.

import { motion } from "motion/react";
import { useState } from "react";

export default function Example() {
const [showSecond, setShowSecond] = useState(false);

return (
  <div className="wrapper">
    <button className="button" onClick={() => setShowSecond((s) => !s)}>
      Animate
    </button>
    {showSecond ? (
      <div className="second-element" />
    ) : (
      <div className="element" />
    )}
  </div>
);
}

This is obviously a pretty simple example, but we can already see how powerful this is. Shared layout animations also work when you navigate between pages. It opens so many doors to a more native web.

Let’s try and recreate the animated modals that we have seen before.

Here’s the same component, but without a smooth transition. Given what we just talked about, try to recreate the transition yourself.

"use client";

import { useEffect, useState, useRef } from "react";
import { useOnClickOutside } from "usehooks-ts";
import { motion } from "motion/react";

export default function SharedLayout() {
const [activeGame, setActiveGame] = useState(null);
const ref = useRef(null);
useOnClickOutside(ref, () => setActiveGame(null));

useEffect(() => {
  function onKeyDown(event) {
    if (event.key === "Escape") {
      setActiveGame(null);
    }
  }

  window.addEventListener("keydown", onKeyDown);
  return () => window.removeEventListener("keydown", onKeyDown);
}, []);

return (
  <>
    {activeGame ? (
      <>
        <div className="overlay" />
        <div className="active-game">
          <div className="inner" ref={ref} style={{ borderRadius: 12 }}>
            <div className="header">
              <img
                height={56}
                width={56}
                alt=""
                src={activeGame.image}
                style={{ borderRadius: 12 }}
              />
              <div className="header-inner">
                <div className="content-wrapper">
                  <h2 className="game-title">{activeGame.title}</h2>
                  <p className="game-description">{activeGame.description}</p>
                </div>
                <button className="button">Get</button>
              </div>
            </div>
            <p className="long-description">{activeGame.longDescription}</p>
          </div>
        </div>
      </>
    ) : null}
    <ul className="list">
      {GAMES.map((game) => (
        <li
          key={game.title}
          onClick={() => setActiveGame(game)}
          style={{ borderRadius: 8 }}
        >
          <img
            height={56}
            width={56}
            alt=""
            src={game.image}
            style={{ borderRadius: 12 }}
          />
          <div className="game-wrapper">
            <div className="content-wrapper">
              <h2 className="game-title">{game.title}</h2>
              <p className="game-description">{game.description}</p>
            </div>
            <button className="button">Get</button>
          </div>
        </li>
      ))}
    </ul>
  </>
);
}

const GAMES = [
{
  title: "The Oddysey",
  description: "Explore unknown galaxies.",
  longDescription:
    "Throughout their journey, players will encounter diverse alien races, each with their own unique cultures and technologies. Engage in thrilling space combat, negotiate complex diplomatic relations, and make critical decisions that affect the balance of power in the galaxy.",
  image:
    "https://animations.dev/how-i-use-framer-motion/how-i-code-animations/space.png",
},
{
  title: "Angry Rabbits",
  description: "They are coming for you.",
  longDescription:
    "The rabbits are angry and they are coming for you. You have to defend yourself with your carrot gun. The game is not simple, you have to be fast and accurate to survive.",
  image:
    "https://animations.dev/how-i-use-framer-motion/how-i-code-animations/rabbit.png",
},
{
  title: "Ghost town",
  description: "Find the ghosts.",
  longDescription:
    "You are in a ghost town and you have to find the ghosts. But be careful, they are dangerous.",
  image:
    "https://animations.dev/how-i-use-framer-motion/how-i-code-animations/ghost.webp",
},
{
  title: "Pirates in the jungle",
  description: "Find the treasure.",
  longDescription:
    "You are a pirate and you have to find the treasure in the jungle. But be careful, there are traps and wild animals.",
  image:
    "https://animations.dev/how-i-use-framer-motion/how-i-code-animations/pirate.png",
},

{
  title: "Lost in the mountains",
  description: "Find your way home.",
  longDescription:
    "You are lost in the mountains and you have to find your way home. But be careful, there are dangerous animals and you can get lost.",
  image:
    "https://animations.dev/how-i-use-framer-motion/how-i-code-animations/boy.webp",
},
];

Gestures

Framer Motion exposes a simple yet powerful set of UI gesture recognisers. That means that we can make a draggable component by simply adding the drag prop to a motion element for example.

As you can see in the demo above, the draggable element maintains momentum when dragging finishes, which helps it feel more natural. Usually, we want a simple drag functionality though. We can disable this effect by setting dragMomentum to false.

"use client";

import { motion } from "motion/react";
import { useRef } from "react";

export function DragExample() {
  const boundingBox = useRef(null);

  return (
    <div ref={boundingBox} className="h-64 w-full p-6">
      <motion.div
        drag
        // this prevents the element from being dragged outside of the bounding box
        dragConstraints={boundingBox}
		dragMomentum={false}
        className="h-10 w-10 rounded-full bg-gray-400"
      />
    </div>
  );
}
"use client";

import { motion } from "motion/react";
import { useRef } from "react";

export function DragExample() {
  const boundingBox = useRef(null);

  return (
    <div ref={boundingBox} className="h-64 w-full p-6">
      <motion.div
        drag
        // this prevents the element from being dragged outside of the bounding box
        dragConstraints={boundingBox}
		dragMomentum={false}
        className="h-10 w-10 rounded-full bg-gray-400"
      />
    </div>
  );
}

App Store-like transition

Initially, this component was a separate lesson. But, I realized that it’s very similar to the shared layout exercise we did before. The design is just different. We will cover a Feedback popover instead, which isn’t as similar to the shared layout exercise and touches a few interesting topics.

Keep in mind that it’s not finished, there is a small text jump when you click on the card and in dark mode, there’s a slight white border present when animating. I might come back to it in the future and cover it more in-depth.

Game of the day

A game about vikings

Are you ready? A game about vikings, where you can play as a viking and fight other vikings. You can also build your own viking village and explore the world.

The never ending adventureIn this game set in a fairy tale world, players embark on a quest through mystical lands filled with enchanting forests and towering mountains.

I did prepare the code for the App Store-like transition, so if you want to try it out, it’s right below. We won’t do an explanation video on this one, it’s just pure code. You can see it as an extra exercise. Try reading through the code and understanding it. If you have any questions, feel free to email me, I’m always happy to help.

Keep in mind that the drag interaction is not added here yet due to time constraints. I’ll add it in the near future though!

"use client";

import { useState, useEffect, useRef } from "react";
import { AnimatePresence, motion } from "motion/react";
import "./styles.css";
import { useOnClickOutside } from "usehooks-ts";

function Card({ card, setActiveCard }) {
return (
  <motion.div
    layoutId={`card-${card.title}`}
    className="card"
    whileTap={{ scale: 0.98 }}
    onClick={() => setActiveCard(card)}
    style={{ borderRadius: 20 }}
  >
    <motion.img
      layoutId={`image-${card.title}`}
      src={card.image}
      alt=""
      style={{ borderRadius: 20 }}
    />
    <motion.button
      aria-hidden
      tabIndex={-1}
	layoutId={`close-button-${card.title}`}
      className="close-button"
      style={{ opacity: 0 }}
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
        strokeWidth="2"
        height="20"
        width="20"
        stroke="currentColor"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          d="M6 18 18 6M6 6l12 12"
        />
      </svg>
    </motion.button>
    <motion.div
      layoutId={`card-content-${card.title}`}
      className="card-content"
    >
      <div className="card-text">
        <motion.h2
          layoutId={`card-heading-${card.title}`}
          className="card-heading"
        >
          Game of the day
        </motion.h2>
      </div>
      <motion.div
        layoutId={`card-extra-info-${card.title}`}
        className="extra-info"
        style={{ borderBottomLeftRadius: 20, borderBottomRightRadius: 20 }}
      >
        <motion.img
          src={card.logo}
          width={40}
          height={40}
          alt=""
          layoutId={`card-game-image-${card.title}`}
          className="rounded-lg"
        />
        <div className="desc-wrapper">
          <motion.span
            layoutId={`card-game-title-${card.title}`}
            className="game-title"
          >
            {card.title}
          </motion.span>
          <motion.span
            layoutId={`card-game-subtitle-${card.title}`}
            className="game-subtitle"
          >
            {card.description}
          </motion.span>
        </div>
        <motion.button
          layoutId={`card-button-${card.title}`}
          className="get-button"
        >
          Get
        </motion.button>
      </motion.div>
    </motion.div>

    <motion.div
      layoutId={`card-long-description-${card.title}`}
      className="long-description"
      style={{ position: "absolute", top: "100%", opacity: 0 }}
    >
      <div>
        <p>
          <b>Are you ready?</b> {card.longDescription}
        </p>
        <p>
          <b>The never ending adventure</b>
          In this game set in a fairy tale world, players embark on a quest
          through mystical lands filled with enchanting forests and towering
          mountains.
        </p>
      </div>
    </motion.div>
  </motion.div>
);
}

function ActiveCard({ activeCard, setActiveCard }) {
const ref = useRef(null);
useOnClickOutside(ref, () => setActiveCard(null));

return (
  <motion.div
    ref={ref}
    layoutId={`card-${activeCard.title}`}
    className="card card-active"
    style={{ borderRadius: 0 }}
  >
    <div className="card-inner">
      <motion.img
        layoutId={`image-${activeCard.title}`}
        src={activeCard.image}
        alt=""
        style={{ borderRadius: 0 }}
      />
      <motion.button
        layoutId={`close-button-${activeCard.title}`}
        className="close-button"
        aria-label="Close button"
        onClick={() => setActiveCard(null)}
      >
        <svg
          xmlns="http://www.w3.org/2000/svg"
          fill="none"
          viewBox="0 0 24 24"
          strokeWidth="2"
          height="20"
          width="20"
          stroke="currentColor"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            d="M6 18 18 6M6 6l12 12"
          />
        </svg>
      </motion.button>
      <motion.div
        layoutId={`card-content-${activeCard.title}`}
        className="card-content active-card-content"
      >
        <div className="card-text">
          <motion.h2
            layoutId={`card-heading-${activeCard.title}`}
            layout
            className="card-heading"
          >
            Game of the day
          </motion.h2>
        </div>
        <motion.div
          layoutId={`card-extra-info-${activeCard.title}`}
          className="extra-info"
          style={{ borderBottomLeftRadius: 20, borderBottomRightRadius: 20 }}
        >
          <motion.img
            src={activeCard.logo}
            width={40}
            height={40}
            alt=""
            layoutId={`card-game-image-${activeCard.title}`}
            className="rounded-lg"
          />
          <div className="desc-wrapper">
            <motion.span
              layoutId={`card-game-title-${activeCard.title}`}
              className="game-title"
            >
              {activeCard.title}
            </motion.span>
            <motion.span
              layoutId={`card-game-subtitle-${activeCard.title}`}
              className="game-subtitle"
            >
              {activeCard.description}
            </motion.span>
          </div>
          <motion.button
            layoutId={`card-button-${activeCard.title}`}
            layout
            className="get-button"
          >
            Get
          </motion.button>
        </motion.div>
      </motion.div>
    </div>

    <motion.div
      layoutId={`card-long-description-${activeCard.title}`}
      className="long-description"
    >
      <p>
        <b>Are you ready?</b> {activeCard.longDescription}
      </p>
      <p>
        <b>The never ending adventure </b>
        In this game set in a fairy tale world, players embark on a quest
        through mystical lands filled with enchanting forests and towering
        mountains. Players can explore the world, build their own viking
      </p>
    </motion.div>
  </motion.div>
);
}

export default function StyledWithoutDrag() {
const [activeCard, setActiveCard] = useState(null);

useEffect(() => {
  function onKeyDown(event) {
    if (event.key === "Escape") {
      setActiveCard(null);
    }
  }

  window.addEventListener("keydown", onKeyDown);
  return () => window.removeEventListener("keydown", onKeyDown);
}, []);

return (
  <div className="cards-wrapper">
    {CARDS.map((card) => (
      <Card key={card.title} card={card} setActiveCard={setActiveCard} />
    ))}
    <AnimatePresence>
      {activeCard ? (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          className="overlay"
        />
      ) : null}
    </AnimatePresence>
    <AnimatePresence>
      {activeCard ? (
        <ActiveCard activeCard={activeCard} setActiveCard={setActiveCard} />
      ) : null}
    </AnimatePresence>
  </div>
);
}

const CARDS = [
{
  title: "Vikings",
  subtitle: "Clash of the Norse Warriors",
  description: "A game about vikings",
  longDescription:
    "A game about vikings, where you can play as a viking and fight other vikings. You can also build your own viking village and explore the world.",
  image:
    "https://animations.dev/how-i-use-framer-motion/app-store-like-cards/game.webp",
  logo: "https://animations.dev/how-i-use-framer-motion/app-store-like-cards/game-logo.webp",
},
];

That’s it

These are the features of Framer Motion that I use the most. When you see one of my animations posted on Twitter, there’s a high chance it was done using the features we talked about in this lesson.

There are more features in Framer Motion, like reorder, useAnimate, etc. We will touch on some of them in the upcoming lessons. But in this module we talk about how I use Framer Motion, and I simply don’t use everything it offers. So we’ll focus on the features I think are the most useful.