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 accross 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 nice examples of animating auto on its site but neither really demonstrate animating auto in an unconstrained context (that is no limit on it’s height and/or width).
What we’ll be building today is an accordion which upon toggling, gets the height of it’s 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);
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();
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)"
});
Now for the exciting part; configuring our springs. We’re using two here. One for the container and other 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]);
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>
);
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: