Skip to main content
Have you ever tried to animate a container’s width or height with Motion and run into this? The container just snaps to its new size. The text animates in fine, but the container itself jumps. That’s because width and height are not animatable when set to auto. The browser doesn’t know how to interpolate between a fixed value and “whatever the content needs.” A smoother approach has the container animate to fit the content. So how do we achieve this?

Building a useMeasure Hook

Before we get into the animation part, we need a way to track an element’s dimensions. The browser has a native API for this called ResizeObserver. It watches an element and fires a callback whenever its size changes. We can wrap this in a small hook.
import { useCallback, useEffect, useState } from "react";

function useMeasure() {
  const [element, setElement] = useState(null);
  const [bounds, setBounds] = useState({ width: 0, height: 0 });

  const ref = useCallback((node) => {
    setElement(node);
  }, []);

  useEffect(() => {
    if (!element) return;

    const observer = new ResizeObserver(([entry]) => {
      setBounds({
        width: entry.contentRect.width,
        height: entry.contentRect.height,
      });
    });

    observer.observe(element);
    return () => observer.disconnect();
  }, [element]);

  return [ref, bounds];
}
The hook returns a ref callback and a bounds object. Attach the ref to any element and bounds will always reflect its current width and height. When the content inside that element changes and causes a resize, the observer fires, state updates, and your component re-renders with the new values.
There are libraries like react-use-measure that do this too, but as you can see it’s only a few lines of code. No dependency needed.

Integrating with Motion

Now for the animation. The core idea is two divs. An outer div that you animate, and an inner div that you measure. Attach the ref from useMeasure to the inner div, the hook gives you bounds.width and bounds.height which update whenever the content changes, pass those values to Motion’s animate prop on the outer div.
import { motion } from "motion/react";

function AnimatedContainer({ children }) {
  const [ref, bounds] = useMeasure();

  return (
    <motion.div animate={{ height: bounds.height }}>
      <div ref={ref}>{children}</div>
    </motion.div>
  );
}

Animating Width

Buttons that change their label are a common use case. Loading states, confirmation messages, multi-step forms. Without animated bounds the button jumps between widths. With this pattern, it slides.
import { motion } from "motion/react";
import { useState } from "react";

function AnimatedButton() {
  const [ref, bounds] = useMeasure();
  const [label, setLabel] = useState("Click me");

  return (
    <motion.button
      animate={{
        width: bounds.width > 0 ? bounds.width : "auto"
      }}
      transition={{ type: "spring", stiffness: 300, damping: 30 }}
    >
      <div ref={ref}>{label}</div>
    </motion.button>
  );
}
The trick here is the ref goes on the inner wrapper, not the button itself. The button’s width is controlled by Motion. The wrapper’s width is controlled by its content. When the content changes, the measured width updates, and the button smoothly resizes to fit.
One thing to note is that I’m checking bounds.width > 0 before animating. This avoids an animation from 0 on the initial render. You want the first frame to just show the button at its natural size, not animate in from nothing.

Animating Height

Height is where this pattern really shines. Expandable sections, accordions, FAQs, details panels. Anywhere content reveals or hides itself. The same pattern applies. Measure the inner content, animate the outer container.
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";

function AnimatedList() {
  const [ref, bounds] = useMeasure();
  const [items, setItems] = useState(["Item 1", "Item 2", "Item 3"]);

  return (
    <motion.div
      animate={{
        height: bounds.height > 0 ? bounds.height : "auto"
      }}
      transition={{ type: "spring", stiffness: 300, damping: 30 }}
      style={{ overflow: "hidden" }}
    >
      <div ref={ref}>
        <AnimatePresence mode="popLayout">
          {items.map((item, index) => (
            <motion.div
              key={item}
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
            >
              {item}
            </motion.div>
          ))}
        </AnimatePresence>
      </div>
    </motion.div>
  );
}
When items are added or removed, the measured height changes and the animation follows. Works for any dynamic content: text, images, lists, nested components.

Gotchas

On initial render, bounds.width and bounds.height will be 0 before the first measurement. Guard against this or you’ll get an animation from 0 to the actual size on mount. A simple bounds.height > 0 ? bounds.height : "auto" works.
Don’t measure and animate the same element. The ref goes on the inner div, the animate prop goes on the outer div. If you put both on the same element you create a loop: the animation changes the size, which triggers a new measurement, which triggers a new animation.
Add a small delay to the animation to give the feel of a natural transition catching up to the content.
Finally, don’t overuse this pattern. It’s a subtle effect that should be used sparingly. Use it when it makes sense, like for buttons, accordions, or other interactive elements.

Resources

Build docs developers (and LLMs) love