How to animate width and height with framer motion

while preventing layout jumps

If you clicked on this snippet is likely that you are trying to transition content smoothly without making the surrounding content jump around.

Let's first make sure you understand how to animate the height and width properties and then we will move onto avoiding layout jumps.

Animating height and width

Like other properties, width and height can be animated via the animate prop.

import { useState } from "react";
import { motion } from "framer-motion";

export default function BasicExample ()  {
  const [isBig, setIsBig] = useState(false);

  return (
    <div className="flex flex-col justify-center items-center space-y-7 p-10">
      <motion.div
        className=" bg-[#ACECA1] rounded-3xl"
        animate={{
          height: isBig ? 200 : 100,
          width: isBig ? 200 : 100,
        }}
      ></motion.div>
      <button
        onClick={() => setIsBig((isBig) => !isBig)}
        className="bg-[#446DF6] font-sans text-2xl text-white border-none p-[10px] cursor-pointer rounded-lg"
      >
        {isBig ? "Make it small" : "Make it big"}
      </button>
    </div>
  );
};

Pretty simple :)

Animating from 0 to auto

Ok so the above is great but most of the time we want to animate height in order to smooth the transition of content. Here is an example:

import { motion, AnimatePresence } from "framer-motion"
import { useState } from "react"

import { FAQ } from "./types"
import { defaultFAQs } from "./defaultValues"
import { More, Less } from "./svgs"

function FAQItem(props: FAQ) {
    const [isOpen, setIsOpen] = useState(false);
  
    return (
      <button
        className="flex flex-col text-left w-full bg-[#EFEFEF] p-3 rounded-lg"
        onClick={() => setIsOpen((prev) => !prev)}
      >
        <div className="flex justify-between items-center w-full">
          <div className="text-2xl font-semibold">{props.question}</div>
          <AnimatePresence initial={false} mode="wait">
            <motion.div
              key={isOpen ? "minus" : "plus"}
              initial={{
                rotate: isOpen ? -90 : 90,
              }}
              animate={{
                zIndex: 1,
                rotate: 0,
                transition: {
                  type: "tween",
                  duration: 0.15,
                  ease: "circOut",
                },
              }}
              exit={{
                zIndex: 0,
                rotate: isOpen ? -90 : 90,
                transition: {
                  type: "tween",
                  duration: 0.15,
                  ease: "circIn",
                },
              }}
            >
              {isOpen ? <Less /> : <More />}
            </motion.div>
          </AnimatePresence>
        </div>
        {isOpen && <div className="text-lg font-light">{props.answer}</div>}
      </button>
    );
  }
  
  export default function FAQComponent() {
    return (
      <div className="flex flex-col w-full p-5 justify-center items-center space-y-7">
        {defaultFAQs.map((FAQ, i) => (
          <FAQItem key={i} {...FAQ} />
        ))}
      </div>
    );
  }

This FAQ component is a bit jarring due to the layout shift of toggling a question "open". Thankfully Framer Motion allows us to animate our height from 0px to auto.

<motion.div
    initial={{
        height: 0,
        opacity: 0,
    }}
    animate={{
        height: "auto",
        opacity: 1,
    }}
    exit={{
        height: 0,
        opacity: 0,
    }}
>
    {props.answer}
</div>
import { motion, AnimatePresence } from "framer-motion"
import { useState } from "react"

import { FAQ } from "./types"
import { defaultFAQs } from "./defaultValues"
import { More, Less } from "./svgs"

const FAQItem = (props: Faq) => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <button
      className="flex flex-col text-left w-full bg-[#EFEFEF] p-3 rounded-lg"
      onClick={() => setIsOpen((prev) => !prev)}
    >
      <div className="flex justify-between items-center w-full">
        <div className="text-2xl font-semibold">{props.question}</div>
        <AnimatePresence initial={false} mode="wait">
          <motion.div
            key={isOpen ? "minus" : "plus"}
            initial={{
              rotate: isOpen ? -90 : 90,
            }}
            animate={{
              zIndex: 1,
              rotate: 0,
              transition: {
                type: "tween",
                duration: 0.15,
                ease: "circOut",
              },
            }}
            exit={{
              zIndex: 0,
              rotate: isOpen ? -90 : 90,
              transition: {
                type: "tween",
                duration: 0.15,
                ease: "circIn",
              },
            }}
          >
            {isOpen ? <Less /> : <More />}
          </motion.div>
        </AnimatePresence>
      </div>
      <AnimatePresence>
        {isOpen && (
          <motion.div
            initial={{
              height: 0,
              opacity: 0,
            }}
            animate={{
              height: "auto",
              opacity: 1,
            }}
            exit={{
              height: 0,
              opacity: 0,
            }}
            key={props.answer}
            className="text-lg font-light"
          >
            {props.answer}
          </motion.div>
        )}
      </AnimatePresence>
    </button>
  );
};
  
  export default function FAQComponent() {
    return (
      <div className="flex flex-col w-full p-5 justify-center items-center space-y-7">
        {defaultFAQs.map((FAQ, i) => (
          <FAQItem key={i} {...FAQ} />
        ))}
      </div>
    );
  }

Adding better transition

The only thing really left in this example is to update the transition properties of our animation so that the text doesn't get visually clipped by our height.

 <motion.div
    initial={{
        height: 0,
        opacity: 0,
    }}
    animate={{
        height: "auto",
        opacity: 1,
+       transition: {
+           height: {
+                duration: 0.4,
+           },
+           opacity: {
+                duration: 0.25,
+                delay: 0.15,
+           },
        },
    }}
    exit={{
        height: 0,
        opacity: 0,
+       transition: {
+           height: {
+               duration: 0.4,
+           },
+           opacity: {
+               duration: 0.25,
+           },
+       },
    }}
    key="test"
    className="text-lg font-light"
>
    {props.description}
</motion.div>

On exit, we are setting the opacity duration so that it finishes before the height gets too small, and on animate, we are delaying the opacity animation so that it starts after the height animation has gotten a head starts.

import { motion, AnimatePresence } from "framer-motion"
import { useState } from "react"

import { FAQ } from "./types"
import { defaultFAQs } from "./defaultValues"
import { More, Less } from "./svgs"

const FAQItem = (props: Faq) => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <button
      className="flex flex-col text-left w-full bg-[#EFEFEF] p-3 rounded-lg"
      onClick={() => setIsOpen((prev) => !prev)}
    >
      <div className="flex justify-between items-center w-full">
        <div className="text-2xl font-semibold">{props.question}</div>
        <AnimatePresence initial={false} mode="wait">
          <motion.div
            key={isOpen ? "minus" : "plus"}
            initial={{
              rotate: isOpen ? -90 : 90,
            }}
            animate={{
              zIndex: 1,
              rotate: 0,
              transition: {
                type: "tween",
                duration: 0.15,
                ease: "circOut",
              },
            }}
            exit={{
              zIndex: 0,
              rotate: isOpen ? -90 : 90,
              transition: {
                type: "tween",
                duration: 0.15,
                ease: "circIn",
              },
            }}
          >
            {isOpen ? <Less /> : <More />}
          </motion.div>
        </AnimatePresence>
      </div>
      <AnimatePresence mode="wait">
        {isOpen && (
          <motion.div
            initial={{
              height: 0,
              opacity: 0,
            }}
            animate={{
              height: "auto",
              opacity: 1,
              transition: {
                height: {
                  duration: 0.4,
                },
                opacity: {
                  duration: 0.25,
                  delay: 0.15,
                },
              },
            }}
            exit={{
              height: 0,
              opacity: 0,
              transition: {
                height: {
                  duration: 0.4,
                },
                opacity: {
                  duration: 0.25,
                },
              },
            }}
            key="test"
            className="text-lg font-light"
          >
            {props.answer}
          </motion.div>
        )}
      </AnimatePresence>
    </button>
  );
};

export default function FAQComponent () {
  return (
    <div className="flex flex-col w-full p-5 justify-center items-center space-y-7">
      {defaultFAQs.map((c, i) => (
        <FAQItem key={i} {...c} />
      ))}
    </div>
  );
};

I am working on a breakdown of this Accordion component with further dive into Framer Motion's layout API to do this animation with a transform. If that sounds interesting to you subscribe below.

Hope it was helpful! If you still are confused HMU with any feedback you have at the email in the footer :)

Don't forget to subscribe for more!

delivered right to your inbox

Contact me at

my reddit accountmy codepen accountmy twitter accountmy github account