CSS is an easy and declarative tool to implement styling and quick. For the most part, you can make an app look great without touching a single line of code. Awesome! But it does raise a few limitations with animations, however. Without having the ability to obtain a reference to the target element itself, and/or the data that is bound to it, animations cannot be pixel perfect. Indeed, some developers are aware of that, and consequently, have come up with various libraries and frameworks, of which D3 is included.
D3 allows you to bind arbitrary data to a Document Object Model (DOM), and then apply data-driven transformations to the document — D3
So yeah, D3 is great, but if you know a thing or two about it, you’ve probably realized that it’s a hell of a complicated library. And so do other libraries/frameworks that come with a lot of overhead, I believe. So let’s take all these 3rd party assets out of the picture for a moment and try to really understand the underlying mechanism for creating pixel perfect animations.
(TL;DR) The secret lies within
Element.prototype.getBoundingClientRect(), probably one of the least discussed methods out there. All it really does is calculating the position and dimensions of the rectangle that is bound to the target element:
So how exactly can I use this method to create pixel perfect animations? And how can it be done in the context of React? Let’s take the following use case as an example. Given that I have 3 entities, foo, bar, and baz, to which the scalars 20, 60, and 35 are bound respectively, I would like to create a nicely transitioned bar chart component. Here’s a snapshot of the final state of the bar chart, once the transition has finished:
Using CSS only, I would have no other alternative but to specify a fixed duration for the animation. So I can pick an arbitrary number, and try to adjust it until I get something to my liking. Here’s how it would look like:
It does look nice, but any time I look at it, I feel like scratching my head. Some bars are bigger than the others, so accordingly, shouldn’t they take longer to transition? (I know, that’s opinionated; but we’re here to learn).
Roughly speaking, the duration of the animation can be calculated by multiplying the height of bounding client rectangle by its expansion velocity (px/ms). So given one of the bar elements from the chart in question, here’s how I would calculate its animation duration based on a velocity of 1.2px/ms:
If the above bounding client rectangle would have a height of 500px, the duration of the animation would be ~416ms. So moving on to the second phase of this article — how can it be done in the context of React? I broke down the solution to the following:
- React finishes updating its virtual DOM.
- At the layout phase, right before drawing anything to the screen, wet the dimensions of the element with
- We apply an animation at a rate of 2px/ms by setting a duration that is derived from the element’s height.
Behold the final result:
Note that I used
useEffet() is highly encouraged for majority of use cases, it would actually result in a stuttering frame right before the transition.
Updates scheduled inside
useLayoutEffectwill be flushed synchronously, before the browser has a chance to paint — React
If you’ll think creatively you’ll discover that the design pattern in this article can be applied in many use cases and can be stretched even further. While you’re at it, I recommend you to look at: