Framer Motion

Trash interaction

To get the codebase running locally, you first need to download it here. We switched from a GitHub repo, because it would be quite problematic to keep it private.

Framer Motion Trash interaction

Installation

To get the codebase running locally, you first need to download it here. We switched from a GitHub repo, because it would be quite problematic to keep it private.

After you have it on your device, you can run pnpm install and pnpm dev after.

The final code can be downloaded here.

Toolbar animation

The toolbar appears once we select at least one image, and it disappears when we deselect all images. Because we need to hide it, we should wrap our toolbar in AnimatePresence and add an appropriate animation to it.

<AnimatePresence>
  {imagesToRemove.length > 0 && !readyToRemove ? (
    <motion.div
      key="toolbar"
      initial={{ y: 20, opacity: 0, filter: "blur(4px)" }}
      animate={{
        y: 0,
        opacity: 1,
        filter: "blur(0px)",
      }}
      exit={{ y: 20, opacity: 0, filter: "blur(4px)" }}
      transition={{ duration: 0.3, bounce: 0, type: "spring" }}
      className="absolute bottom-8 flex gap-1 rounded-xl p-1 shadow-md"
    >
      <div className="flex w-full justify-between gap-1">
        <button className="flex w-12 flex-col items-center gap-px rounded-lg bg-gray-200 pb-1 pt-[6px] text-[10px] font-medium text-gray-900">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            className="size-4 shrink-0"
            viewBox="0 0 24 24"
            fill="none"
          >
            <path
              fillRule="evenodd"
              clipRule="evenodd"
              d="M10.8839 18.6339C10.3957 19.122 9.60427 19.122 9.11612 18.6339L3.36612 12.8839C3.1317 12.6495 3 12.3315 3 12C3 11.6685 3.13169 11.3506 3.36612 11.1161L9.11612 5.36612C9.60427 4.87796 10.3957 4.87796 10.8839 5.36612C11.372 5.85427 11.372 6.64573 10.8839 7.13388L7.26776 10.75H19.75C20.4404 10.75 21 11.3097 21 12C21 12.6904 20.4404 13.25 19.75 13.25H7.26777L10.8839 16.8661C11.372 17.3543 11.372 18.1457 10.8839 18.6339Z"
              fill="currentColor"
            />
          </svg>
          Back
        </button>
        <button
          onClick={() => {
            if (readyToRemove) {
              setRemove(true);
            } else {
              setReadyToRemove(true);
            }
          }}
          className="flex w-12 flex-col items-center gap-px rounded-lg bg-gray-200 pb-1 pt-[6px] text-[10px] font-medium text-gray-900 hover:bg-red-200 hover:text-red-900"
        >
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
            fill="none"
            className="size-4 shrink-0"
          >
            <path
              fillRule="evenodd"
              clipRule="evenodd"
              d="M7.58393 5C8.28068 3.24301 9.99487 2 12.0009 2C14.007 2 15.7212 3.24301 16.4179 5H21.25C21.6642 5 22 5.33579 22 5.75C22 6.16421 21.6642 6.5 21.25 6.5H19.9532L19.0588 20.3627C18.9994 21.2835 18.2352 22 17.3124 22H6.68756C5.76481 22 5.0006 21.2835 4.94119 20.3627L4.04683 6.5H2.75C2.33579 6.5 2 6.16421 2 5.75C2 5.33579 2.33579 5 2.75 5H7.58393ZM9.26161 5C9.83935 4.09775 10.8509 3.5 12.0009 3.5C13.151 3.5 14.1625 4.09775 14.7403 5H9.26161Z"
              fill="currentColor"
            />
          </svg>
          Trash
        </button>
        <button className="flex w-12 flex-col items-center gap-px rounded-lg bg-gray-200 pb-1 pt-[6px] text-[10px] font-medium text-gray-900">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            className="size-4"
            viewBox="0 0 24 24"
            fill="none"
          >
            <path
              fillRule="evenodd"
              clipRule="evenodd"
              d="M10.4902 2.84406C11.1661 1.69 12.8343 1.69 13.5103 2.84406L22.0156 17.3654C22.699 18.5321 21.8576 19.9999 20.5056 19.9999H3.49483C2.14281 19.9999 1.30147 18.5321 1.98479 17.3654L10.4902 2.84406ZM12 9C12.4142 9 12.75 9.33579 12.75 9.75V13.25C12.75 13.6642 12.4142 14 12 14C11.5858 14 11.25 13.6642 11.25 13.25V9.75C11.25 9.33579 11.5858 9 12 9ZM13 15.75C13 16.3023 12.5523 16.75 12 16.75C11.4477 16.75 11 16.3023 11 15.75C11 15.1977 11.4477 14.75 12 14.75C12.5523 14.75 13 15.1977 13 15.75Z"
              fill="currentColor"
            />
          </svg>
          Report
        </button>
      </div>
    </motion.div>
  ) : null}
</AnimatePresence>;
<AnimatePresence>
  {imagesToRemove.length > 0 && !readyToRemove ? (
    <motion.div
      key="toolbar"
      initial={{ y: 20, opacity: 0, filter: "blur(4px)" }}
      animate={{
        y: 0,
        opacity: 1,
        filter: "blur(0px)",
      }}
      exit={{ y: 20, opacity: 0, filter: "blur(4px)" }}
      transition={{ duration: 0.3, bounce: 0, type: "spring" }}
      className="absolute bottom-8 flex gap-1 rounded-xl p-1 shadow-md"
    >
      <div className="flex w-full justify-between gap-1">
        <button className="flex w-12 flex-col items-center gap-px rounded-lg bg-gray-200 pb-1 pt-[6px] text-[10px] font-medium text-gray-900">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            className="size-4 shrink-0"
            viewBox="0 0 24 24"
            fill="none"
          >
            <path
              fillRule="evenodd"
              clipRule="evenodd"
              d="M10.8839 18.6339C10.3957 19.122 9.60427 19.122 9.11612 18.6339L3.36612 12.8839C3.1317 12.6495 3 12.3315 3 12C3 11.6685 3.13169 11.3506 3.36612 11.1161L9.11612 5.36612C9.60427 4.87796 10.3957 4.87796 10.8839 5.36612C11.372 5.85427 11.372 6.64573 10.8839 7.13388L7.26776 10.75H19.75C20.4404 10.75 21 11.3097 21 12C21 12.6904 20.4404 13.25 19.75 13.25H7.26777L10.8839 16.8661C11.372 17.3543 11.372 18.1457 10.8839 18.6339Z"
              fill="currentColor"
            />
          </svg>
          Back
        </button>
        <button
          onClick={() => {
            if (readyToRemove) {
              setRemove(true);
            } else {
              setReadyToRemove(true);
            }
          }}
          className="flex w-12 flex-col items-center gap-px rounded-lg bg-gray-200 pb-1 pt-[6px] text-[10px] font-medium text-gray-900 hover:bg-red-200 hover:text-red-900"
        >
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
            fill="none"
            className="size-4 shrink-0"
          >
            <path
              fillRule="evenodd"
              clipRule="evenodd"
              d="M7.58393 5C8.28068 3.24301 9.99487 2 12.0009 2C14.007 2 15.7212 3.24301 16.4179 5H21.25C21.6642 5 22 5.33579 22 5.75C22 6.16421 21.6642 6.5 21.25 6.5H19.9532L19.0588 20.3627C18.9994 21.2835 18.2352 22 17.3124 22H6.68756C5.76481 22 5.0006 21.2835 4.94119 20.3627L4.04683 6.5H2.75C2.33579 6.5 2 6.16421 2 5.75C2 5.33579 2.33579 5 2.75 5H7.58393ZM9.26161 5C9.83935 4.09775 10.8509 3.5 12.0009 3.5C13.151 3.5 14.1625 4.09775 14.7403 5H9.26161Z"
              fill="currentColor"
            />
          </svg>
          Trash
        </button>
        <button className="flex w-12 flex-col items-center gap-px rounded-lg bg-gray-200 pb-1 pt-[6px] text-[10px] font-medium text-gray-900">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            className="size-4"
            viewBox="0 0 24 24"
            fill="none"
          >
            <path
              fillRule="evenodd"
              clipRule="evenodd"
              d="M10.4902 2.84406C11.1661 1.69 12.8343 1.69 13.5103 2.84406L22.0156 17.3654C22.699 18.5321 21.8576 19.9999 20.5056 19.9999H3.49483C2.14281 19.9999 1.30147 18.5321 1.98479 17.3654L10.4902 2.84406ZM12 9C12.4142 9 12.75 9.33579 12.75 9.75V13.25C12.75 13.6642 12.4142 14 12 14C11.5858 14 11.25 13.6642 11.25 13.25V9.75C11.25 9.33579 11.5858 9 12 9ZM13 15.75C13 16.3023 12.5523 16.75 12 16.75C11.4477 16.75 11 16.3023 11 15.75C11 15.1977 11.4477 14.75 12 14.75C12.5523 14.75 13 15.1977 13 15.75Z"
              fill="currentColor"
            />
          </svg>
          Report
        </button>
      </div>
    </motion.div>
  ) : null}
</AnimatePresence>;

Transition into the trash

We’ll need to use the layoutId prop here. That way we tell Framer Motion that the images in the grid should become the images in the trash. Let’s add the prop to both the grid and the trash images.

<motion.img
  layoutId={`image-${image}`}
  src={`/how-i-use-framer-motion/why-framer-motion/${image}.webp`}
  height={65}
  width={65}
  style={{
    borderRadius: 6,
    rotate:
      index % 2 === 0
        ? 4 * (imagesToRemove.length - index + 1)
        : -1 * (imagesToRemove.length - index + 1) * 4,
  }}
/>;
<motion.img
  layoutId={`image-${image}`}
  src={`/how-i-use-framer-motion/why-framer-motion/${image}.webp`}
  height={65}
  width={65}
  style={{
    borderRadius: 6,
    rotate:
      index % 2 === 0
        ? 4 * (imagesToRemove.length - index + 1)
        : -1 * (imagesToRemove.length - index + 1) * 4,
  }}
/>;

Trash enter and exit animation

We’ll wrap our trash with AnimatePresence and add a small animation to it.

<motion.div
  initial={{ opacity: 0, filter: "blur(4px)", scale: 1.2 }}
  animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
  exit={{ opacity: 0, filter: "blur(4px)", scale: 1.2 }}
  transition={{ type: "spring", duration: 0.2, bounce: 0 }}
>
  <TrashBack />
</motion.div>;
<motion.div
  initial={{ opacity: 0, filter: "blur(4px)", scale: 1.2 }}
  animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
  exit={{ opacity: 0, filter: "blur(4px)", scale: 1.2 }}
  transition={{ type: "spring", duration: 0.2, bounce: 0 }}
>
  <TrashBack />
</motion.div>;

Instant removal fix

If we don’t select all images, the unselected ones disappear instantly. To fix that, we can wrap our images grid in AnimatePresence and add an exit animation to it (the addition of an exit animation is not included in the video).

<motion.li
  key={image}
  className="relative flex"
  exit={
    !isSelected
      ? {
          opacity: 0,
          filter: "blur(4px)",
          transition: { duration: 0.05 },
        }
      : {}
  }
>
<motion.li
  key={image}
  className="relative flex"
  exit={
    !isSelected
      ? {
          opacity: 0,
          filter: "blur(4px)",
          transition: { duration: 0.05 },
        }
      : {}
  }
>

Images are going through the trash

To fix the issue with images going through the trash, we can ensure that our <TrashFront /> component becomes visible after our images are almost done animating. We need to time it essentially.

<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  exit={{ opacity: 0 }}
  transition={{
    duration: 0,
    delay: 0.175,
  }}
  className="absolute bottom-0 left-[3px] h-full w-[90px]"
>
  <TrashFront />
</motion.div>;
<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  exit={{ opacity: 0 }}
  transition={{
    duration: 0,
    delay: 0.175,
  }}
  className="absolute bottom-0 left-[3px] h-full w-[90px]"
>
  <TrashFront />
</motion.div>;

Motion Config

I’m not the biggest fan of the transition right now, but I don’t want to adjust it for each item separately. This is a perfect use case for MotionConfig.

<MotionConfig transition={{ type: "spring", duration: 0.5, bounce: 0.2 }}>
  // ...
</MotionConfig>
<MotionConfig transition={{ type: "spring", duration: 0.5, bounce: 0.2 }}>
  // ...
</MotionConfig>

Dropping into the bin

We now need to create this nice drop animation. However, we can’t influence the way shared layout animations are done. What we can do is we can animate the parent div so that the images will follow it.

<motion.div
  animate={{
    y: 73,
  }}
  transition={{ delay: 0.13 }}
  className="absolute flex w-full top-[-60px] flex-col-reverse items-center"
>
 // ...
</motion.div>;
<motion.div
  animate={{
    y: 73,
  }}
  transition={{ delay: 0.13 }}
  className="absolute flex w-full top-[-60px] flex-col-reverse items-center"
>
 // ...
</motion.div>;

Key takeaways

  • We can animate the parent of our shared layout animation to influence how the children are animated.
  • MotionConfig is a great way to set a default transition for all animations in a component.