Exit Animations problem in Route Transitions with framer-motion - reactjs

I have been trying to animate route transitions using framer-motion but can't wrap my head around the exit animations. Entry animations work as they should.
I want the component to slide in from the left and slide to the right when route changes.
I havewrapped the Switch component using AnimatedPresence from framer-motion and used a location object and taken its pathname as its key.
Wrapped all of this using BrowserRouter
Ensured I have used motion component from framer-motion at the root element of each of my functional components
Codesandbox
App.js
function App() {
const location = useLocation();
return (
<div className="App">
<AnimateTrans loc={location} />
</div>
);
}
AnimateTrans.jsx
export default function (props) {
return (
<AnimatePresence exitBeforeEnter>
<Switch location={props.loc} key={props.loc.pathname}>
<Route exact path="/">
<First />
</Route>
<Route exact path="/sec">
<Second />
</Route>
</Switch>
</AnimatePresence>
);
}
First.jsx
const transition = {
duration: 1,
type: "spring"
};
const transVariants = {
init: { scale: 0.3, opacity: 1, x: -400, transition: { transition } },
anim: { scale: 1, opacity: 1, x: 0, transition: { transition } },
exit: { scale: 0.4, opacity: 0, x: 400, transition: { transition } }
};
return (
<motion.div
variants={transVariants}
initial="init"
animate="anim"
exit="exit"
className="container"
>
<div className="content">
<img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ1a79XgKvvi2VyAu54LI21MbKrfSlZTL9iPA&usqp=CAU" />
</div>
<div className="oth"></div>
d
</motion.div>
);
Second.jsx
const transition = {
duration: 1,
type: "spring"
};
const transVariants = {
initial: { scale: 0.3, opacity: 1, x: -400, transition: { transition } },
animate: { scale: 1, opacity: 1, x: 0, transition: { transition } },
exit1: { scale: 0.4, opacity: 0, x: 400, transition: { transition } }
};
return (
<motion.div
variants={transVariants}
initial="initial"
animate="animate"
exit="exit1"
className="container"
>
<div className="content">
<img src="https://fashioneditorials.com/wp-content/uploads/2017/10/i-D-Japan-Chanel-Haute-Couture-With-Nana-Komatsu-Angelo-Pennetta-1.jpg" />
</div>
<div className="oth"></div>
d
</motion.div>
);
}
Is my location object at fault here? Should I change the key or structure App.js differently?

You are using the <a> tag to route, you need to use Link from react-router-dom. Then It's gonna work.

Related

Framer motion and exit property not working with InertiaJs

I would like to know how can I use "exit" property with framer motion and InertiaJs / ReactJs please.
Currently I have two pages :
profil.js
const { pathname } = window.location
...
<div>
<motion.div className="flex flex-col" layoutId={`title-3`}
initial={{opacity: 0}}
animate={{
y: [-20, 0],
opacity: [0, 1]
}}
exit={{scale: 2}}
key={{pathname}}
transition={{duration: 0.2, ease: "easeOut", delay: 0.1}}
>
<span>Profil</span>
</motion.div>
</div>
home.js
const { pathname } = window.location
...
<div>
<motion.div className="flex flex-col" layoutId={`title-3`}
initial={{opacity: 0}}
animate={{
y: [-20, 0],
opacity: [0, 1]
}}
exit={{scale: 2}}
key={{pathname}}
transition={{duration: 0.2, ease: "easeOut", delay: 0.1}}
>
<span>Home</span>
</motion.div>
</div>
And I set AnimateSharedLayout on my App (app.js) :
import { AnimateSharedLayout } from 'framer-motion';
return render(
<Provider store={store}>
<AnimateSharedLayout>
<App {...props} />
</AnimateSharedLayout>
</Provider>, el
);
When I click on "home" or "profil" in the sidebar, I see the animate, but I don't see the exit animation.
I use InertiaJs and ReactJs.
Do you have an idea why I don't see the exit ?
Once you click away from "home" or "profile", that element is removed from the DOM right away, so it won't be there long enough to see the animation.
Framer Motion Docs
By wrapping motion components with AnimatePresence, they gain the use of an exit property that can define either a set of values or a variant name to animate to before being removed.
Try wrapping your <motion.div>s with <AnimatePresence> like this:
const { pathname } = window.location
...
<div>
<AnimatePresence>
<motion.div className="flex flex-col" layoutId={`title-3`}
initial={{opacity: 0}}
animate={{
y: [-20, 0],
opacity: [0, 1]
}}
exit={{scale: 2}}
key={{pathname}}
transition={{duration: 0.2, ease: "easeOut", delay: 0.1}}
>
<span>Home</span>
</motion.div>
</AnimatePresence>
</div>

Avoiding framer-motion initial animations on mount

Please see this codesandbox.
I have a basic framer-motion animation where the height of a box is animated when toggled. However, I want the box to be shown by default, but when the page loads the initial animation is presented.
My question is, how do I avoid having an initial animation for a component if it should be shown on mount, but still maintain future enter and exit animations? Thanks!
I came up with this kind of solution;
1- I took variants to inside of the component
2 - I created two states for opacity and height
3 - States are initially same as where you animate to. So basically nothing happens when you first render.
4 - With useEffect, you can swap the values with the actual initial values, so after first render, the animation works.
export const AnimatedFallback = ({ isVisible }) => {
const [opacity, setOpacity] = useState(1);
const [height, setHeight] = useState("200px");
const variants = {
initial: {
opacity: opacity,
height: height
},
enter: {
opacity: 1,
height: "200px",
transition: { duration: 0.5 }
},
exit: {
opacity: 0,
height: 0,
transition: { duration: 0.5 }
}
};
useEffect(()=> {
setHeight(0)
setOpacity(0)
}, [])
return (
<AnimatePresence>
{isVisible && (
<motion.div
animate="enter"
className="fallback"
exit="exit"
initial="initial"
variants={variants}
>
Suspense Fallback Component
</motion.div>
)}
</AnimatePresence>
);
};
You can check if it is the components first render by:
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
});
If it is the first render of the component, don't pass the variants to the motion.div. In your example this would look like this:
const variants = {
initial: {
opacity: 0,
height: 0
},
enter: {
opacity: 1,
height: "200px",
transition: { duration: 0.5 }
},
exit: {
opacity: 0,
height: 0,
transition: { duration: 0.5 }
}
};
export const AnimatedFallback = ({ isVisible }) => {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
});
return (
<AnimatePresence>
{isVisible && (
<motion.div
animate="enter"
className="fallback"
exit="exit"
initial="initial"
variants={firstRender.current ? {} : variants}
>
Suspense Fallback Component
</motion.div>
)}
</AnimatePresence>
);
};
Bit old of a question, but just in case some people stumble upon this. if you are using an AnimatePresence you can use initial={false} on the AnimatePresence component. Like so,
<AnimatePresence initial={false}>
{someCondition ? (
<motion.h1 {...yourProps}>
yooo
</motion.h1>
) : null}
</AnimatePresence>
More info here:
https://www.framer.com/docs/animate-presence/##suppressing-initial-animations
The simplest method is provided in the framer motion official docs:
specifiy the initial prop with the value as false
<motion.div
...
initial={false}
>
</motion.div>
https://www.framer.com/docs/animation/##enter-animations
For me specifying initial property in motion.div helped
<motion.div initial={{ opacity: 0, height: 0}}>
...
</motion.div>

How can i switch between react components using framer motion?

In my react app i need to switch between components like in a carousel. I found this example to build an image carousel only using framer motion: https://codesandbox.io/s/framer-motion-image-gallery-pqvx3?file=/src/Example.tsx:1715-1725
I want to adapt this to switching between components. At the moment my page looks something like this:
const variants = {
enter: (direction: number) => {
return {
x: direction > 0 ? 100 : -100,
opacity: 0,
}
},
center: {
zIndex: 1,
x: 0,
opacity: 1,
},
exit: (direction: number) => {
return {
zIndex: 0,
x: direction < 0 ? 100 : -100,
opacity: 0,
}
},
}
const Page = () => {
const [[page, direction], setPage] = useState([0, 0])
const paginate = (newDirection: number) => {
setPage([page + newDirection, newDirection])
}
return (
<motion.div
key={page}
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
>
<!-- my components, between which I want to switch, should appear here -->
</motion.div>
)
}
how would I have to build the logic to be able to switch dynamic between my components (slides)? In the codesandbox example the images were changed via an array:
const imageIndex = wrap(0, images.length, page);
<motion.img key={page} src={images[imageIndex]} />
How could i do that to switch between jsx elements?
Edit
The answer from Joshua Wootonn is correct, but you need to add the custom prop also to the TestComp to get the animation working with dynamic variants like this:
const TestComp = ({ bg }: { bg: string }) => (
<motion.div
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{
x: { type: "spring", stiffness: 100, damping: 30 },
opacity: { duration: 0.2 },
}}
className="absolute w-full h-full"
style={{
background: bg,
}}
/>
)
A couple of things were missing from the above answer to get exit animations working.
If you want exit animations to work within AnimationPresense you need to set keys on its children
<AnimatePresence initial={false} custom={direction}>
{page === 0 && <TestComp key="0" bg="rgb(171, 135, 255)" />}
{page === 1 && <TestComp key="1" bg="rgb(68, 109, 246)" />}
{page === 2 && <TestComp key="2" bg="rgb(172, 236, 161)" />}
</AnimatePresence>
If you want to animate something in while something is still animating out without having massive content shifting, you need to take them out of the flow. (use absolute positioning and wrap with relatively positioned container)
<div style={{ position: "relative", height: "300px", width: "300px" }}>
<AnimatePresence initial={false} custom={direction}>
...
</AnimatePresence>
</div>
and on the child components
height: 100%;
width: 100%;
position: absolute;
Working codesandbox: https://codesandbox.io/s/framer-motion-carousel-animation-wetrf?file=/src/App.tsx:658-708
Your components should return <motion.div> (or <motion.section>, <motion.span> etc.).
And in the page component you should use <AnimatePresence /> component (like in the example):
<AnimatePresence initial={false} custom={direction}>
{COMPONENTS}
</AnimatePresence>
Then you have to decide which component will appear:
{page === 0 && <ComponentOne />}
{page === 1 && <ComponentTwo/>}
{page === 2 && <ComponentThree/>}
The animations you can control with variants.
You can see a quick demo here: https://codesandbox.io/s/quizzical-hypatia-7wqjc?file=/src/App.tsx

Exit animation for children items not working in Framer Motion

I am making a hamburger menu animation in React using Framer Motion. When I click on the hamburger menu, the side drawer and the navigation items slide in from the left.
When I click on the close menu icon, then the whole side drawer slides out to the left(which means the exit prop on SideDrawer component is working).
What do I want?
When I click on the close icon, I want the navigation items to slide out first and then the side drawer. I have tried adding exit prop to children navigation items. But it does not work.
How can I achieve the desired effect?
Code snippets are as below:
src/App.js
import React, { useState } from "react";
import "./App.css";
import Menu from "./components/Menu";
import SideDrawer from "./components/SideDrawer";
import Overlay from "./components/Overlay";
const App = () => {
const [menuOpen, setMenuOpen] = useState(false);
const handleMenuClick = () => {
setMenuOpen(!menuOpen);
};
return (
<div className="App">
<Menu menuOpen={menuOpen} onMenuClick={handleMenuClick} />
<SideDrawer menuOpen={menuOpen} />
<Overlay menuOpen={menuOpen} />
</div>
);
};
export default App;
src/components/Menu.js
import React, { useState } from "react";
import { motion } from "framer-motion";
const lineOneVariants = {
initial: { rotate: "0deg" },
animate: { y: ".8rem", rotate: "45deg", transformOrigin: "center center" },
};
const lineTwoVariants = {
initial: { opacity: 1 },
animate: { opacity: 0 },
};
const lineThreeVariants = {
initial: { rotate: "0deg" },
animate: { y: "-.8rem", rotate: "-45deg", transformOrigin: "center center" },
};
const Menu = ({ onMenuClick, menuOpen }) => {
return (
<div className="hamburger_menu">
<div className="hamburger_menu-line-container" onClick={onMenuClick}>
<motion.div
variants={lineOneVariants}
initial="initial"
animate={menuOpen ? "animate" : "initial"}
className="hamburger_menu-line-1"
></motion.div>
<motion.div
variants={lineTwoVariants}
initial="initial"
animate={menuOpen ? "animate" : "initial"}
className="hamburger_menu-line-2"
></motion.div>
<motion.div
variants={lineThreeVariants}
initial="initial"
animate={menuOpen ? "animate" : "initial"}
className="hamburger_menu-line-3"
></motion.div>
</div>
</div>
);
};
export default Menu;
src/components/Overlay.js
import React from "react";
import { motion } from "framer-motion";
const overlayVariants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
};
const Overlay = ({ menuOpen }) => {
return (
<motion.div
variants={overlayVariants}
initial="initial"
animate={menuOpen ? "animate" : "initial"}
className="overlay"
></motion.div>
);
};
export default Overlay;
src/components/SideDrawer.js
import React from "react";
import { motion, AnimatePresence } from "framer-motion";
const drawerVariants = {
initial: {
x: "-100vw",
opacity: 0,
},
animate: {
x: 0,
opacity: 1,
transition: {
type: "linear",
ease: "easeInOut",
staggerChildren: 0.1,
delayChildren: 0.15,
},
},
};
const drawerMenuVariants = {
initial: { x: "-5rem", opacity: 0 },
animate: {
x: 0,
opacity: 1,
transition: {
type: "linear",
ease: "easeInOut",
},
},
};
const SideDrawer = ({ menuOpen }) => {
return (
<AnimatePresence>
{menuOpen && (
<motion.div
variants={drawerVariants}
initial="initial"
animate="animate"
exit="initial"
className="sideDrawer"
>
<nav className="sideDrawer-menu">
<ul>
<motion.li
variants={drawerMenuVariants}
whileHover={{ scale: 1.05 }}
>
Register/Login
</motion.li>
<motion.li
variants={drawerMenuVariants}
whileHover={{ scale: 1.05 }}
>
About
</motion.li>
<motion.li
variants={drawerMenuVariants}
whileHover={{ scale: 1.05 }}
>
Projects
</motion.li>
<motion.li
variants={drawerMenuVariants}
whileHover={{ scale: 1.05 }}
>
CV
</motion.li>
</ul>
</nav>
</motion.div>
)}
</AnimatePresence>
);
};
export default SideDrawer;
You need to tell the side drawer to wait until the exit animation for the children finishes before starting its own exit animation.
You can do this by using the when property of the transition. See Orchestration in the docs.
In your case, you'd add it to the initial variant of your drawerVariants, since that's the variant you animate to on exit:
initial: {
x: "-100vw",
opacity: 0,
transition: {
when: "afterChildren"
}
},
You probably also want to add some staggering and easing there if you want to mirror the animate in behavior, but I'll leave that up to you.

React-spring Transition didn't repeat animation

In my react app I have component, where I using Transition from react-spring.
import { Transition } from 'react-spring'
export class myComponent {
state = {
items: []
}
static getDerivedStateFromProps(props, state){
const newItems = [...props.items]
if(props.items !== state.items){
return {items: newItems}
}
}
render() {
const { items } = this.state
<Transition
items={items}
keys={item => items.id}
initial={{ opacity: 1, height: 'auto' }}
from={{ opacity: 0, height: 0 }}
enter={{ opacity: 1, height: 'auto' }}
leave={{ opacity: 0, height: 0 }}
>
{item => propsAnimations => (
<section style={propsAnimations}>
<Item
key={item.selectionId}
/>
</section>
)}
</Transition>
}
}
But when I updating this.state.items this is didn't repeat animation.
When I updating items in state, I change only elements order in array.

Resources