How to animate width and height with framer motion

Jun 23, 2021 (Updated: Nov 4, 2022)

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-black"
        initial={false}
        animate={{
          height: isBig ? 200 : 100,
          width: isBig ? 200 : 100,
        }}
      ></motion.div>
      <button
        onClick={() => setIsBig((isBig) => !isBig)}
        className="border-2 border-black text-black font-medium decoration-0 px-2.5 pb-0.5 pt-1 cursor-pointer"
      >
        {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="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="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="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="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 created a further breakdown of how this component works with accessibility upgrades here if you want more details.

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

Subscribe to the newsletter

A monthly no filler update.

Contact me at