Search code examples
javascripttypescriptframer-motion

Framer Motion StaggerChildren Animation on Child Removal do not Trigger Animation


I have a list (ParentBox.tsx) that contains many items (Box.tsx). When clicking the Add button, the ParentBox has one additional unique Box. The animation works fine. However, there are two scenarios where it does not:

  1. When I click on the Box, it removes the item from the list. Framer Motion removes the Box from the user interface without exit animation.
  2. When clicking "Remove All", the whole list of items is removed. There is no exit stagger effect.

I want to have an individual element of the list animated out, and when the whole list is cleared, have them one by one animated out.

Full Repro in CodeSanbox

Parent Box

const variantsBoxContainer: Variants = {
  hidden: {
    transition: {
      staggerChildren: 0.1,
      delayChildren: 0.3,
      staggerDirection: -1
    }
  },
  show: {
    transition: {
      staggerChildren: 0.1,
      delayChildren: 0.3,
      staggerDirection: 1
    }
  }
};

let id = 3;
export const ParentBox = (props: ParentBoxProps) => {
  const [items, setItems] = useState<Item[]>([
    { id: 1, text: "Test #1" },
    { id: 2, text: "Test #2" }
  ]);
  return (
    <motion.div
      className="parentbox"
    >
      <button
        onClick={() => {
          id++;
          setItems([...items, { id: id, text: `Click to delete id ${id}` }]);
        }}
      >
        Add
      </button>
      <button
        onClick={() => {
          id++;
          setItems([]);
        }}
      >
        Remove All
      </button>

      <motion.ol
        variants={variantsBoxContainer}
        initial="hidden"
        animate="show"
        exit="hidden"
      >
        <AnimatePresence mode="popLayout">
          {items
            .sort((a, b) => a.id - b.id)
            .map((d) => (
              <Box
                key={d.id}
                data={d}
                onRemove={(item) => {
                  const newList = items.filter((i) => i.id !== item.id);
                  console.log(newList);
                  setItems(newList);
                }}
              />
            ))}
        </AnimatePresence>
      </motion.ol>
    </motion.div>
  );
};

Box

const variantBox: Variants = {
  hidden: { opacity: 0, top: -100, transition: { duration: 2 } },
  show: { opacity: 1, top: 0, transition: { duration: 2 } }
};
export const Box = (props: BoxProps) => {
  return (
    <motion.li
      className="box"
      variants={variantBox}
      onClick={() => {
        props.onRemove(props.data);
      }}
    >
      {props.data.text}
    </motion.li>
  );
};

What I have tried so far:

  1. Adding/Removing the explicit mention of initial, animate, exit on the Box component.
  2. Adding/Removing the when option.
  3. Tried all mode in the AnimatedPresence
  4. Try to add a function for the hidden (exit) variant to have a custom delay per index
  5. Ensure all Box all have unique key

Let me know if you have any idea what I am missing to have the animation on Box removal (children).

CodeSanbox


Solution

  • Exit animations will work if you explicitly indicate which variant to use for the animation states:

    export const Box = (props: BoxProps) => {
      return (
        <motion.li
          custom={props.index}
          className="box"
          variants={variantBox}
          exit="hidden"
          initial="hidden"
          animate="show"
          onClick={() => {
            props.onRemove(props.data);
          }}
        >
          {props.data.text}
        </motion.li>
      );
    };
    

    I believe AnimatePresence is conflicting with the staggerChildren prop since it appears between the parent and children. See this issue on GitHub.

    Quickest workaround is probably to use dynamic variants and manually set a delay in the variants for the Box component (based on the index in the items array.