Exit animation for children items not working in Framer Motion - reactjs

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.

Related

How to trigger framer motion animation onClick

I am applying a slide in animation using framer motion which slides in the User Card below on page render, but I want to trigger the animation on click of a button
Code below
const wrapperVariants = {
hidden: {
opacity: 0,
x: '100vw'
},
visible: {
opacity: 1,
x: 0,
transition: { type: 'spring', delay: 0.1 }
},
exit: {
x: "-100vh",
transition: { ease: 'easeInOut' }
}
};
<motion.div
variants={wrapperVariants}
initial="hidden"
animate="visible"
exit="exit"
>
<UserCard />
</motion.div>
<button onClick={() => {
I want to Trigger the Animation on button click not just on render
}}></button>
You can use animation control hook in framer motion and for your case it would be sth like this :
const wrapperVariants = {
hidden: {
opacity: 0,
x: '100vw',
},
visible: {
opacity: 1,
x: 0,
transition: { type: 'spring', delay: 0.1 },
},
exit: {
x: '-100vh',
transition: { ease: 'easeInOut' },
},
};
const controls = useAnimationControls();
<motion.div variants={wrapperVariants} initial="hidden" animate={controls} exit="exit">
<UserCard />
</motion.div>
<button onClick={() => controls.start('visible')}></button>
You could do something like
const [clicked, setClicked] = useState(false)
<motion.div
variants={wrapperVariants}
initial="hidden"
animate= {clicked ? 'visible' : 'hidden'}
exit="exit"
>
<button onClick={() => setClicked(!clicked)}></button>

How can I stop all the videos in the array playing at the start?

So I am using React Player. I have set up a toggle button so that I can play and pause the video. This all works fine. What I would like to do is have the first video from an array of three videos play as soon as the page loads. The problem I am having is that all the videos play. Even though you can only actually see the first video playing, if I hit pause then I can hear the audio from the other videos. What should I do to fix this?
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { wrap } from "popmotion";
import ReactPlayer from "react-player";
export default function VideoSlider(props) {
const { videos } = props;
const [[page, direction], setPage] = useState([0, 0]);
const videoIndex = wrap(0, videos.length, page);
const paginate = (newDirection) => {
setPage([page + newDirection, newDirection]);
};
let timeout;
const handleClick = (direction) => {
paginate(direction);
};
console.log("VIDEO", videos[videoIndex]);
const [playing, setPlaying] = React.useState(true);
const play = () => setPlaying(true);
const pause = () => setPlaying(false);
return (
<motion.div className="videpSliderWrapper">
<AnimatePresence initial={false} custom={direction}>
<ReactPlayer
playing={playing}
onPlay={play}
onPause={pause}
className="videoSlider"
key={page}
url={videos[videoIndex]}
custom={direction}
initial={{
opacity: 0,
transition: {
duration: 0.5,
ease: "easeInOut",
},
}}
animate={{
opacity: 1,
transition: {
duration: 0.5,
ease: "easeInOut",
},
}}
exit={{
opacity: 0,
transition: {
duration: 0.5,
ease: "easeInOut",
},
}}
width="100%"
height="auto"
></ReactPlayer>
</AnimatePresence>
<motion.div
className="blankIcon"
initial={{ opacity: 0 }}
whileHover={{ opacity: 1 }}
whileTap={{ opacity: 1 }}
onClick={() => setPlaying(!playing)}
>
{playing ? (
<div className="pauseIcon"></div>
) : (
<div className="playIcon"></div>
)}
</motion.div>
<motion.div
className="next"
whileHover={{
backgroundColor: "#ffffff",
transition: {
duration: 0.5,
ease: "easeInOut",
},
}}
onClick={() => handleClick(1)}
></motion.div>
<motion.div
className="prev"
whileHover={{
backgroundColor: "#ffffff",
transition: {
duration: 0.5,
ease: "easeInOut",
},
}}
onClick={() => handleClick(-1)}
></motion.div>
</motion.div>
);
}

How to stagger children with Framer Motion in a separate file?

I am trying to stagger a list of children div's that are rendered in a separate component from the div that holds them.
Is this an issue of the parent-child relationship not being in the same file?
Here is the structure:
Games.js (Parent)
GameItem.js (Child)
// Games
import { motion } from 'framer-motion'
export const Games = () => {
// Framer Motion Animation
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.5,
},
},
}
// Render child items
const gameItems = games.map((game, index) => (
<GameItem
key={index}
game={game}
index={index}
/>
))
return (
<motion.div
variants={container}
initial="hidden"
animate="show"
>{gameItems}
<motion.div>
)}
// GameItem.js
import { motion } from 'framer-motion'
// Framer Motion Animation
const item = {
hidden: { opacity: 0 },
show: { opacity: 1 },
}
return (
<motion.div
variants={item}
>{game.name}</motion.div>
)}

react-spring transition is not showing when first time rendering

I am using React-spring for first time. I am trying to use transition hook on a side drawer on my page by toggling a button.
But when I am clicking on that button there is no animation as that side drawer opens instantly, but if I click second time then side drawer is closing with animation.
And also if I click that button before that drawer removed from DOM then slide from left animation is there. I can't figure it out where is the problem. Help me please. Thanks.
Here is my code:
import React, { useState } from "react";
import { useTransition, animated, config } from "react-spring";
const Transform = (props) => {
const myStyle = {
position: "fixed",
left: 0,
top: 0,
zIndex: 100,
backgroundColor: "black",
};
const [drawerIsOpen, setDrawerState] = useState(false);
const closeDrawerHandler = () => {
setDrawerState((v) => !v);
};
const transition = useTransition(drawerIsOpen, {
form: { transform: "translateX(-100%)", opacity: 0 },
enter: { transform: "translateX(0%)", opacity: 1 },
leave: { transform: "translateX(-100%)", opacity: 0 },
config: { duration: 2000 },
// config: config.molasses,
// openDrawerHandler: () => setDrawerState(true),
});
return (
<>
{transition((style, item) =>
item ? (
<animated.aside
className='bg-white h-100 w-70 shadow'
style={{ ...style, ...myStyle }}
onClick={closeDrawerHandler}
>
<nav className='h-100'>
<h2>It's a Side Drawer</h2>
</nav>
</animated.aside>
) : (
""
)
)}
<div className='d-flex justify-content-end'>
<button className='btn btn-primary ' onClick={closeDrawerHandler}>
Toggle Btn
</button>
</div>
</>
);
};
export default Transform;
Image of transition problem
I made spelling mistake. Silly one, I spelt from as form . That's why it was happening.

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>

Resources