DEV Community

Chris Berry
Chris Berry

Posted on • Edited on • Originally published at chrisberry.io

Animate Auto with React Spring

Animating auto height or width is always a tricky matter. While there are a number of approaches that get you part way there. Javascript is the only answer that gets us what we’re really looking for.

If you’re using react, then there’s a good chance you’ve already come across React Spring. If you haven’t, be warned, plain old CSS transitions just won’t cut it once you’ve discovered the beauty of physics-based animations.

Now, React Spring does have a couple of nice examples of animating auto on its site but neither really demonstrates animating auto in an unconstrained context (that is no limit on its height and/or width).

What we’ll be building today is an accordion which upon toggling, gets the height of its content and animates to that value. See below for an example of the final product:

So what’s happening here?

Let’s break down the code piece by piece…

The Components State

const defaultHeight = "100px";

// Manages the open or cloased state of the accordion
const [open, toggle] = useState(false);

// The height of the content inside of the accordion
const [contentHeight, setContentHeight] = useState(defaultHeight);
Enter fullscreen mode Exit fullscreen mode

In the above code, we’re using two instances of React’s useState hook. The first holds the “open” state of the accordion (either true or false). The second holds the height of the accordion’s content.

useMeasure

// Gets the height of the element (ref)
const [ref, { height }] = useMeasure();
Enter fullscreen mode Exit fullscreen mode

Next, we have a custom hook provided by the React Use library. useMeasure takes advantage of the Resize Observer API to measure the size of the target container.

React Spring

// Animations
const expand = useSpring({
  config: { friction: 10 },
  height: open ? `${contentHeight}px` : defaultHeight
});
const spin = useSpring({
  config: { friction: 10 },
  transform: open ? "rotate(180deg)" : "rotate(0deg)"
});
Enter fullscreen mode Exit fullscreen mode

Now for the exciting part; configuring our springs. We’re using two here. One for the container and another for the button trigger. One point worth noting is that we're using a template literal to transform the number provided by the useMeasure hook to a string which can be interpolated by React Spring. Another important point to note is that we don't access the value of height directly (we'll get to the reason why shortly).

Get the Height

useEffect(() => {
  //Sets initial height
  setContentHeight(height);

  //Adds resize event listener
  window.addEventListener("resize", setContentHeight(height));

  // Clean-up
  return window.removeEventListener("resize", setContentHeight(height));
}, [height]);
Enter fullscreen mode Exit fullscreen mode

The last piece before our return portion of our component is a useEffect hook. We're using it here to get the height of the accordion content upon the mounting of the component, as well as adding an event listener to update the contentHeight whenever the window is resized. A moment ago, I highlighted the fact that we aren't referencing the height value in our spring. What I've noticed with useMeasure (resize observer) is that it deals in units smaller than pixels. Consequently, even if there is no resize or animation occurring, useMeasure will sometimes report different sizes continuously (e.g. 750.10, 750.90, 750.95). If we had referenced height instead of contentHeight spring would constantly try to animate to the different values. While this may or may not result in performance issues, it just feels wrong to be animating between values which are imperceptible.

The Markup

return (
  <div className={style.wrapper}>
    <animated.div className={style.accordion} style={expand}>
      <div ref={ref} className={style.content}>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit...
        </p>
      </div>
    </animated.div>
    <animated.button
      className={style.expand}
      onClick={() => toggle(!open)}
      style={spin}
    >
      <FontAwesomeIcon icon={faChevronDown} />
    </animated.button>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

The markup of our component is fairly straightforward. The two style attributes are referencing our springs. As React Spring interpolates the values of the CSS properties the styles will, in turn, be updated. For this animating to occur, we need to prepend the element name with animated. The ref on the first child of the first animated.div binds the useMeasure hook to this element. And last but not least, we have the onClick event handler which toggles the open state of our accordion.

Here is the final product:

Top comments (0)