Search code examples
reactjsframer-motion

Framer motion background area between page sliding animation


I have 3 pages which I want to display, clicking next goes to the next page, and back goes to the previous page, and the direction should match the page it is going to. For example going to a previous page should make the previous page slide from left to right.

After a certain combination of user events, the background of the body can be seen while animating. Here is a gif demonstrating the issue:

I use useState to manage the page and the direction it should come from.

This codesandbox has all the relevant code.

Steps to reproduce

Click the buttons in the following order (as shown in the gif): Next, Back, Next, Back``

Code

Here is the code for anyone not willing to visit the codesandbox:

import * as React from "react";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

const xMax = "100vw";
const xMin = "-100vw";

const variants = {
  enter: (direction: number) => ({
    x: direction > 0 ? xMax : xMin,
    transition: { type: "tween", duration: 0.5, ease: "linear" },
  }),
  center: {
    zIndex: 1,
    x: 0,
    transition: { type: "tween", duration: 0.5, ease: "linear" },
  },
  exit: (direction: number) => ({
    zIndex: 0,
    x: direction < 0 ? xMax : xMin,
    transition: { type: "tween", duration: 1, ease: "linear" },
  }),
};

export default function App() {
  const [[page, direction], setPage] = useState([0, 0]);

  const paginate = (newDirection: number) => {
    setPage([page + newDirection, newDirection]);
  };

  return (
    <div
      style={{
        width: "100vw",
        height: "100vh",
        position: "relative",
        overflowX: "hidden",
      }}
    >
      <AnimatePresence initial={false} custom={direction}>
        <motion.div
          key={page}
          custom={direction}
          variants={variants}
          initial="enter"
          animate="center"
          exit="exit"
          style={{
            position: "absolute",
            width: "100%",
            height: "100%",
          }}
        >
          <GetDivs page={page} paginate={paginate} />
        </motion.div>
      </AnimatePresence>
    </div>
  );
}

function GetDivs({
  page,
  paginate,
}: {
  page: number;
  paginate: (newDirection: number) => void;
}) {
  switch (page) {
    case 0:
      return <FirstDiv paginate={paginate} />;
    case 1:
      return <SecondDiv paginate={paginate} />;
  }
  return <ThirdDiv paginate={paginate} />;
}

function FirstDiv({ paginate }: { paginate: (newDirection: number) => void }) {
  return (
    <div
      style={{
        width: "100%",
        height: "100%",
        backgroundColor: "red",
      }}
    >
      <button
        onClick={() => paginate(1)}
        style={{ position: "absolute", right: 20, top: 20 }}
      >
        Next
      </button>
      <p style={{ fontSize: 40, margin: 0 }}>
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Quos, atque
        possimus, est amet eos, dolore impedit incidunt labore veritatis ratione
        eum perferendis exercitationem ex recusandae corrupti illum molestias.
        Minus, corporis! Itaque, sit quaerat? Cumque qui minus voluptatum
        officia nam debitis ipsam corrupti illum voluptatem ipsa, omnis at vitae
        ut cupiditate, placeat vero deserunt quis. Iste eaque itaque cumque
        mollitia voluptate minima sit optio voluptatem similique, facere sunt
        architecto corrupti suscipit officiis tenetur, nihil ipsum. Veniam quae,
        optio maxime atque debitis enim repellendus perspiciatis ipsam nisi iste
        laboriosam excepturi ratione id itaque nihil omnis aperiam. Voluptate
        natus reprehenderit tempore amet, ipsa nemo alias odit dolorum
        voluptatem! Vitae quam deserunt corrupti veritatis consequatur nulla ea
        suscipit quas repellendus minima, dolorem beatae sint iste dolor dicta
        tempore cum quidem assumenda itaque reiciendis fuga voluptatibus! Aut
        tenetur sed distinctio error culpa, quis eligendi possimus deleniti
        quasi. Cumque molestias sit porro saepe vero aliquid. Aspernatur?
      </p>
    </div>
  );
}

function SecondDiv({ paginate }: { paginate: (newDirection: number) => void }) {
  return (
    <div
      style={{
        width: "100%",
        height: "100%",
        backgroundColor: "green",
      }}
    >
      <button
        onClick={() => paginate(-1)}
        style={{ position: "absolute", left: 20, top: 20 }}
      >
        Back
      </button>
      <button
        onClick={() => paginate(1)}
        style={{ position: "absolute", right: 20, top: 20 }}
      >
        Next
      </button>
    </div>
  );
}

function ThirdDiv({ paginate }: { paginate: (newDirection: number) => void }) {
  return (
    <div
      style={{
        width: "100%",
        height: "100%",
        backgroundColor: "blue",
      }}
    >
      <button
        onClick={() => paginate(-1)}
        style={{ position: "absolute", left: 20, top: 20 }}
      >
        Back
      </button>
    </div>
  );
}


Solution

  • I managed to fix it by changing the exit's time to match the duration of the enter's time and manually adjusting the distance moved.

    const variants = {
      enter: (direction: number) => ({
        x: direction > 0 ? xMax : xMin,
        transition: { type: "tween", duration: 0.5, ease: "linear" },
      }),
      center: {
        zIndex: 1,
        x: 0,
        transition: { type: "tween", duration: 0.5, ease: "linear" },
      },
      exit: (direction: number) => ({
        zIndex: 0,
        // x: direction < 0 ? "100vw" : "-100vw", // Before
        // transition: { type: "tween", duration: 1, ease: "linear" }, // Before
        x: direction < 0 ? "50vw" : "-50vw", // After
        transition: { type: "tween", duration: 0.5, ease: "linear" }, // After
      }),
    };
    

    Here is a more general approach:

    const speedFactor = 4;
    const duration = 0.5;
    
    const variants = {
        enter: (direction: number) => {
            return {
                x: direction > 0 ? "100vw" : "-100vw",
                transition: { type: "tween", duration, ease: "linear" },
            };
        },
        center: {
            zIndex: 1,
            x: 0,
            transition: { type: "tween", duration, ease: "linear" },
        },
        exit: (direction: number) => {
            return {
                zIndex: 0,
                x:
                    direction < 0
                        ? `${100 / speedFactor}vw`
                        : `-${100 / speedFactor}vw`,
                transition: { type: "tween", duration, ease: "linear" },
            };
        },
    };