Search code examples
javascriptreactjsanimationframer-motion

Framer Motion (React): How to order initial, exit and layout animations?


I am using framer-motion to animate a change in grid columns.

Here is what I want to do:

  1. I've got nine buttons in a grid (the #db-wrapper is the grid container).

A picture of the website with 9 sound buttons

  1. The user switches to the "maximized" view (props.minimize changes) A fourth column gets added to the grid, the buttons which are already there move to their new positions, leaving empty cells for the remaining buttons. Then, new buttons slide in from the bottom, filling the empty cells. The buttons in the grid are now positioned in consecutive cells.

A picture of the website with 16 sound buttons

  1. Now the user switches back to the minimized view. First, all the buttons that will get deleted should slide out to the bottom.
  2. Then, the remaining buttons should move to their new positions in the grid (their old positions, like at the beginning), so that they fill consecutive cells.

(Basically, I want to do step 1 and 2 backwards after the user has switched back)

I've already achieved step 1 and 2. Here is the functional component:

const DrumButtons = (props) => {
  const drumButtonsVariants = {
    hidden: {
      y: "140vh",
    },
    visible: {
      y: 0,
      transition: {
        type: "tween",
        duration: 1,
        delay: 0.1,
      },
    },
    exit: {
      y: "140vh",
      transition: {
        type: "tween",
        duration: 1,
        delay: 0.1,
      },
    },
  };

  let dbWrapperStyle = {};
  if (!props.minimized) {
    dbWrapperStyle = {
      gridTemplateColumns: "1fr 1fr 1fr 1fr",
    };
  }

  let singleWrapperStyle = {
    width: "100%",
    height: "100%",
  };
  let buttonTransition = { duration: 0.5, delay: 0.1 };
  return (
    <div id="db-wrapper" style={dbWrapperStyle}>
      <AnimatePresence>
        {props.buttonsarr.map((elem, i) => {
          return (
            <motion.div
              variants={drumButtonsVariants}
              initial="hidden"
              animate="visible"
              exit="exit"
              key={elem.press}
              style={singleWrapperStyle}
            >
              <motion.button
                layout
                transition={buttonTransition}
                key={elem.press}
                className="drum-pad"
                onClick={dbHandleClickWrapper(
                  props.changetext,
                  props.buttonsarr
                )}
                id={elem.name}
              >
                <span className="front">{elem.press.toUpperCase()}</span>
                <audio
                  key={elem.press.toUpperCase()}
                  id={elem.press.toUpperCase()}
                  src={props.buttonsarr[i].source}
                  preload="auto"
                  className="clip"
                ></audio>
              </motion.button>
            </motion.div>
          );
        })}
      </AnimatePresence>
    </div>
  );
};

What is the problem, exactly?

So here's what currently happens when switching back from "maximized" to "minimized":

  1. The buttons that are no longer needed slide out to the bottom. At the same time, the 4 grid columns get reduced to 3 columns. So the remaining buttons slide to their position in the 3-column-wide grid, leaving empty cells for the buttons that are currently sliding out, but still present in the DOM.
  2. After the other buttons have been removed, the remaining buttons move again to fill consecutive cells of the grid.

Here is what I've tried:

  1. I've tried adding a when: "beforeChildren" to the transitions of the drumButtonsVariants. Nothing changed.
  2. I've played around with the delays, but I've never achieved the desired outcome. If I delay the layout animation, the removal of the buttons from the DOM will get delayed too, resulting in the same unwanted outcome.

You can view my full code here:

(main branch) https://github.com/Julian-Sz/FCC-Drum-Machine/tree/main

(version with the problem) https://github.com/Julian-Sz/FCC-Drum-Machine/tree/284606cac7cbc4bc6e13bf432c563eab4814d370

Feel free to copy and fork this repository!


Solution

  • The trick is to use State for the dbWrapperStyle. Then, AnimatePresence with onExitComplete can be used.

    This happens when the user switches to minimized again:

    1. The removed buttons slide out (but they are still in the DOM - occupying grid cells)
    2. Now they get removed from the DOM, and onExitComplete fires: The grid columns change to 3 columns and the component gets rerendered. The removal from the DOM and the grid change are happening after each other (without any delay) - resulting in a smooth animation!

    Here is the new component:

    const DrumButtons = (props) => {
      const drumButtonsVariants = {
        visible: {
          y: 0,
          transition: {
            type: "tween",
            duration: 0.8,
            delay: 0.1,
          },
        },
        exit: {
          y: "140vh",
          transition: {
            type: "tween",
            duration: 0.8,
            delay: 0.1,
          },
        },
      };
    
      // BEFORE------------
      // let dbWrapperStyle = {};
      // if (!props.minimized) {
      //   dbWrapperStyle = {
      //     gridTemplateColumns: "1fr 1fr 1fr 1fr",
      //   };
      // }
    
      // AFTER-------------
      const [dbWrapperStyle, setWrapperStyle] = useState({});
      useEffect(() => {
        if (!props.minimized) {
          setWrapperStyle({ gridTemplateColumns: "1fr 1fr 1fr 1fr" });
        }
      }, [props.minimized]);
    
      let singleWrapperStyle = {
        width: "100%",
        height: "100%",
      };
    
      let buttonTransition = {
        type: "spring",
        duration: 0.9,
        delay: 0.1,
        bounce: 0.5,
      };
    
      return (
        <div id="db-wrapper" style={dbWrapperStyle}>
          <AnimatePresence
            onExitComplete={() => {
              setWrapperStyle({ gridTemplateColumns: "1fr 1fr 1fr" });
            }}
          >
            {props.buttonsarr.map((elem, i) => {
              return (
                <motion.div
                  variants={drumButtonsVariants}
                  initial="exit"
                  animate="visible"
                  exit="exit"
                  key={elem.press}
                  style={singleWrapperStyle}
                >
                  <motion.button
                    layout
                    transition={buttonTransition}
                    key={elem.press}
                    className="drum-pad"
                    onClick={dbHandleClickWrapper(
                      props.changetext,
                      props.buttonsarr
                    )}
                    id={elem.name}
                  >
                    <span className="front">{elem.press.toUpperCase()}</span>
                    <audio
                      key={elem.press.toUpperCase()}
                      id={elem.press.toUpperCase()}
                      src={props.buttonsarr[i].source}
                      preload="auto"
                      className="clip"
                    ></audio>
                  </motion.button>
                </motion.div>
              );
            })}
          </AnimatePresence>
        </div>
      );
    };