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.
Click the buttons in the following order (as shown in the gif): Next, Back, Next, Back``
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>
);
}
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" },
};
},
};