cssreactjsframer-motion

Why is there a slight stutter when an AnimatePresence framer.div is removed?


I'm trying to create an Accordion component in React that animates the opening and closing of the "content" section smoothly to expand the height. I've arrived at the below code with the help of some tutorials online and everything seems to be working fine, but there's a noticeable stutter when the "closing" transition on the content div finishes. It opens smoothly, but clicking again causes it to close like 90% of the way smoothly, and then it snaps to the closed state. Why is that, and how might I fix it?

I looked to see whether the layout property might help, but setting it on my accordion component didn't change any behavior. https://www.framer.com/motion/layout-animations/ I thought this might have something to do with it, given that I'm animating the height. I also am unsure of whether the height: auto may be preventing it from animating smoothly. This component is being used in an Astro project, with the client: load prop.

import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Arrow from "/FAQ_Arrow.svg"; // a chevron

export default function Accordion({ header, text }) {
  const [isOpen, toggleOpen] = useState(false);
  return (
    <div className="w-full border-2 border-gray-700 rounded-t-md rounded-md py-4 px-8 bg-white text-black">
      <button
        className="flex justify-between items-center w-full"
        onClick={() => toggleOpen(!isOpen)}
      >
        <h1 className="text-large md:text-2xl font-bold text-[#6130C9]">{header}</h1>
        <img
          src={Arrow}
          alt=""
          className={`transition-all duration-300 ${
            isOpen ? "-rotate-180" : ""
          }`}
        />
      </button>

      {/* Expanded text */}
      <AnimatePresence mode="sync">
        {isOpen && (
          <motion.div
            initial={{ height: 0 }}
            animate={{ height: "fit-content" }}
            exit={{ height: 0 }}
            transition={{duration: 0.3, type: "spring"}}
            className="text-base mt-4 overflow-hidden"
          >
            <h3>{text}</h3>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

I've also put together a basic astro codesandbox that demonstrates the issue: https://codesandbox.io/p/sandbox/vigorous-breeze-d842q8


Solution

  • I ran into a similar issue a while ago. It seems that when framer motion is calculating the height/width to animate, it doesn't take padding, margin, or border width into account. The author of Framer Motion talks more about it in this github issue

    You can fix it by removing all padding and margin from the element entering/leaving the DOM and putting it on an inner div like this:

    <AnimatePresence mode="sync">
        {isOpen && (
          <motion.div
            initial={{ height: 0 }}
            animate={{ height: "fit-content" }}
            exit={{ height: 0 }}
            transition={{duration: 0.3, type: "spring"}}
            className="text-base overflow-hidden"
          >
            <div className="mt-4">
              <h3>{text}</h3>
            </div>
          </motion.div>
        )}
      </AnimatePresence>