DEV Community

Cover image for Animating between units with react-spring
Konstantin Lebedev
Konstantin Lebedev

Posted on • Originally published at konstantinlebedev.com

Animating between units with react-spring

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.

The problem

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.

The solution

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.

Conclusion

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.

Oldest comments (5)

Collapse
 
hermetheus profile image
Allan

Thank you for the tutorial! Exactly the idea I needed for my project!

Collapse
 
kosslebedev profile image
Konstantin Lebedev

Glad you found it helpful! Feel free to share what you'll build, I'm always curious to see animations in the web apps

Collapse
 
chiangs profile image
Stephen Chiang • Edited

React-spring is magical...I use it on my data visualization transitions and really makes it pop.

Collapse
 
dannyboynyc profile image
Daniel Deverell

Great mini tutorial. Helped me out a lot. Thanks!

Collapse
 
kosslebedev profile image
Konstantin Lebedev

Awesome, happy to hear that!