reactjsanimationframer-motionreact-animations

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. enter image description here

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 }}
              >
                <a href="#">Register/Login</a>
              </motion.li>
              <motion.li
                variants={drawerMenuVariants}
                whileHover={{ scale: 1.05 }}
              >
                <a href="#">About</a>
              </motion.li>
              <motion.li
                variants={drawerMenuVariants}
                whileHover={{ scale: 1.05 }}
              >
                <a href="#">Projects</a>
              </motion.li>
              <motion.li
                variants={drawerMenuVariants}
                whileHover={{ scale: 1.05 }}
              >
                <a href="#">CV</a>
              </motion.li>
            </ul>
          </nav>
        </motion.div>
      )}
    </AnimatePresence>
  );
};

export default SideDrawer;


Solution

  • 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.