It's no secret, that on the web we have to deal with different units - from rems and pixels to percentages and viewport-based values. In this tutorial, we'll explore the problem of animating between different units, and see how we can overcome it.
Let's start by creating this simple animation where a div with pixel-based size expands to fill the entire viewport width and height when we click on it:
To create this animation, we'll use
useSpring hook from
react-spring package, and set the width and the height of the box to 200px when it's not expanded, and to 100vh and 100vw when it is. We'll also remove 10px border-radius when the box is expanded:
The result will look like this:
As we can see, the border-radius animation is working, but the box gets smaller instead. Why is that?
To understand the problem, we need to look at how
react-spring (and most of React animation libraries for that matter) handles animation between units. When we pass width and height values as strings,
react-spring will parse the numeric values from the "from" and "to" values, take the unit from the "from" value, and completely ignore the unit of the "to" value:
In our example, the initial state of the box is collapsed and the height of the box is pixel-based, so when
react-spring starts animating it, it'll use "pixels" as a unit. If instead the initial state was expanded and the height was viewport-based, then the animation would use "vh" as a unit and run from 100vh to 200vh instead.
The border-radius animation works fine because if uses pixels for both expanded and collapsed states.
Side note: the only library that I found that handles animation between units correctly out-of-the-box is framer-motion. I have an intro level article about framer-motion, and you can also sign up for my upcoming framer-motion course.
To fix this problem, we need to make sure that both the initial and the target value use the same unit. We can easily convert viewport-based values into pixels with these simple calculations:
Now instead of using viewport-based values, we'll use our helper functions to set the width and the height of the box:
This solves the problem only partially because if we resize the browser window after the animation has run, we'll discover a different issue - the box doesn't adjust to the viewport size anymore since now it has pixel-based size:
We can fix this issue by setting the box size back to viewport-based values once the animation finishes. First of all, we'll use
useRef hook to hold a reference to the actual DOM node of our box. Secondly,
react-spring provides a handy
onRest callback that fires at the end of each animation, so we can use it to check if we animated to the expanded state, and if so, we'll set the box width and height directly.
With this setup, animation works fine - it uses pixel values while animating, and sets the box dimensions to viewport-based size upon completion, so the box remains responsive even if we resize the browser afterward.
You can find working CodeSandbox demo here.
Animation libraries such as
react-spring give us a greater degree of control over our animations compared to CSS animations, but they have shortcomings as well. Animating values between units is one of them, and it requires us to do extra work to make sure that our animation runs smoothly and remains responsive.