Identifying The Problem

Let’s say you have something like:

We have a parent ArticleList component which takes a list of articles as its props. It maps through them, in order, and renders them.

If that list order changes (examples: the user toggles a setting that changes the sorting, an item gets upvoted and changes position, new data comes in from the server…), React reconciles the two states, and destroys / removes / appends / creates nodes as needed.

The DOM is dumb. If an item is removed from its original location and re-inserted at a new location 200px down, it has no awareness about what that update means for the element’s on-screen position.

Because the element’s CSS properties haven’t changed, there is no way to use CSS transitions to animate this change.

Edit note: Originally, this article stated that DOM nodes couldn’t be moved, only created/destroyed. It turns out I was wrong; operations like .appendChild() appear to preserve the element. Luckily, my misunderstanding doesn’t affect the end result, in our case.

How can we get the browser to behave as though these elements have moved? The solution to this problem will take us on a ride through low-level DOM operations, React lifecycle methods, and hardware-accelerated CSS practices. There will even be some basic maths!

The solution

TL:DR — I made a React module that does this.

Source | Demo

To solve this problem, there are a few pieces of info we need, and we need them at a very specific moment in time. Let’s forego the complexity in acquiring them, for now, and operate on the assumptions that:

We know that React just re-rendered , and the DOM nodes have been re-arranged.

, and the DOM nodes have been re-arranged. The browser hasn’t painted yet. Even though the DOM nodes are in their new positions, the on-screen elements haven’t been updated yet.

Even though the DOM nodes are in their new positions, the on-screen elements haven’t been updated yet. We know where the elements are, on the screen.

We know where the elements are about to be re-painted.

Here’s what our situation might be: We have a list of 3 items, and they were just reversed. We know their original position (left side), and we know where they’re moving to (right side).

Kindly ignore my lack of artistic ability

Order of Operations

A quick aside: It may surprise you to learn that there exists a moment in time where we can tell where an item will be before it has been painted to the screen.

When you think about it, it makes sense; how can the browser paint new pixels to the screen before it knows exactly where to paint them?

Thankfully, this is not a black box; The browser updates in distinct steps, and it is possible to execute logic between calculating the layout and painting to the screen.

But how do we access the calculated layout?

DOMRects to the rescue!

Javascript has an incredibly helpful native method, getBoundingClientRect(). It gives us the size and position of any given element, relative to the viewport. Here’s what it might give us if we called it on that top blue rectangle, before the new layout is calculated:

blueItem.getBoundingClientRect()

>> {

>> top: 0,

>> bottom: 600,

>> left: 0,

>> right: 500,

>> height: 60,

>> width: 400

>> }

And, after the new layout is calculated:

blueItem.getBoundingClientRect()

>> {

>> top: 136,

>> bottom: 464,

>> left: 0,

>> right: 500,

>> height: 60,

>> width: 400

>> }

getBoundingClientRect is smart enough to work out the new layout position of an element, taking into account its height, margin, and any other variables that will affect where it is in the viewport.

Armed with these two pieces of data, we can work out the change in the element’s position; its delta.

Δy = finalTop - initialTop = 132 - 0 = 132

So, we know that the element has moved down 132px. Similarly, we know that the middle item hasn’t moved at all (Δy = 0px), and the last item has moved up by 132px (Δy = -132px).

The problem is, while we know all these facts, the DOM is about to update; In a fraction of a second, those boxes will instantly be in their new position!

This is where the next tool in our arsenal comes in: requestAnimationFrame.

This is a method on the window object that tells the browser “Hey, before you paint any changes to the screen, can you run this bit of code first?”. It’s a way to quickly make any adjustments needed before the elements are updated.

What if, before the browser paints, we apply the inverse of the change? Imagine this CSS:

.blue-item {

top: -132px

} .purple-item {

top: 0;

} .fuscia-item {

top: 132px;

}

The browser would paint this update, but the paint wouldn’t change anything; The DOM nodes have changed places, but we’ve offset that change with CSS.

Note: Don’t yell at me yet, experienced front-end devs! We’ll discover a more performant way below to do this re-arranging. I’m just using the top property right now because it’s easier to understand.

This is tricky business, so let’s do a high-level overview of what just happened:

React renders our initial state, with the blue item on top. We use getBoundingClientRect to figure out where the items are positioned. React receives new props: the items have been reversed! Now the blue item is on the bottom. We use getBoundingClientRect to figure out where the items are now, and calculate the change in positions. We use requestAnimationFrame to tell the DOM to apply some CSS that undoes this new change; If the element’s new position is 100px lower, we apply CSS to make it 100px higher.

It’s Animation Time

Ok, so we’ve definitely accomplished something here; we’ve made it so that DOM changes are completely invisible to the user. This might be a neat party trick, but it’s probably still not clear how this helps us.

The thing is, we’ve made it so that we’re in a situation where regular CSS transitions can work again. To animate these elements to their new position, we can just add a transition and undo the artificial position changes.

Continuing with our example above: Our blue item is actually the final item, but it appears to be the first one. Its CSS looks like this:

.blue-item {

top: -132px;

}

Now, let’s update the CSS so it looks like this:

.blue-item {

transition: top 500ms;

top: 0;

}

The blue item will now slide down, over half a second, from the top position to the bottom position. Huzzah! We’ve animated something.

This technique was popularized by Google’s Paul Lewis, and he calls it the FLIP technique. FLIP is an acronym for First, Last, Inverse, Play.

No, not that kind of flip.

Calculate the First position.

position. Calculate the Last position.

position. Invert the positions

the positions Play the animation

Our version is a little different, but it’s the same principle.