Animating the Unanimatable.

Smooth reordering transitions in React.js

Joshua Comeau
Feb 15, 2016 · 9 min read

Between CSS3 transitions, @keyframe animations, and wonderful new technologies like the upcoming Web Animations API, it’s never been easier to make smooth, jank-free animations.

There’s still one thing that none of these technologies can handle out of the box, though; animated list reordering.

Image for post
Image for post
List reordering. Live Example

Identifying The Problem

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.
  • 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.
  • 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).

Image for post
Image for post
Kindly ignore my lack of artistic ability

Order of Operations

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!

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:

  1. React renders our initial state, with the blue item on top. We use getBoundingClientRect to figure out where the items are positioned.
  2. React receives new props: the items have been reversed! Now the blue item is on the bottom.
  3. We use getBoundingClientRect to figure out where the items are now, and calculate the change in positions.
  4. 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

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.

Image for post
Image for post
No, not that kind of flip.
  • Calculate the First position.
  • Calculate the Last position.
  • Invert the positions
  • Play the animation

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

A Brief Foray into the DOM

Originally, browsers did everything with the CPU. In recent years, some very smart people figured out that certain tasks could be delegated to the GPU for massive gains in performance; specifically, when the “texture” of a static piece of content doesn’t change.

The primary objective was to speed up scrolling; when you scroll down a page, none of the elements are changing, they’re just sliding up. The browser people were kind enough to also allow certain css properties to work the same way.

By using the transform suite of CSS properties — translate, scale, rotate, skew, etc — and opacity, we aren’t changing the texture of an element. And if the texture doesn’t change, it doesn’t have to be re-painted on every frame; it can be composited around by the GPU. This is how to achieve 60+fps animations.

Want proof that using this property just sends the element as a texture to the GPU? Try applying a non-uniform scale (eg. transform: scale(1, 2) ) to an element with some text in it.

If you’d like to learn more about the browser’s rendering process (and you should! It’s as fascinating as it is practical), I’ve included some links below.

The missing piece: React

There are two important things that each child needs for this to function:

  1. Every child needs a unique ‘key’ property. This is what we’ll use to tell them apart.
  2. Every child needs a ref, so that we’ll be able to look up the DOM node and calculate its bounding box.

Note: The code snippets below are greatly simplified, and should not be used as-is. For a more complete implementation, see the React Flip Move source.

Getting the First position

At the end of this lifecycle method, our state will be full of DOMRect objects, outlining exactly where every child is on the page.

Getting the Last position

The very important distinction to make here is that React’s render method doesn’t immediately paint to the screen. I’m a little fuzzy on the lower-level details, but the process looks a little something like this:

  • render returns a representation of what it would like the DOM to be,
  • React reconciles this representation with the actual state of the DOM, and applies the differences,
  • The browser notices that something has changed, and calculates the new layout,
  • React’s componentDidUpdate lifecycle method fires,
  • The browser paints the changes to the screen.

The beautiful thing about this process is we have the opportunity to hook into the DOM’s state after its layout is calculated, but before the screen has been updated.

Here’s what that looks like:

Inverting

We’ll use requestAnimationFrame to ensure our changes make it in before that frame.

Let’s continue writing the componentDidUpdate method:

You’ll notice we’re using the more-performant transform: translate property here, instead of messing with top/bottom. The effect is the same, but the animation is much smoother, for reasons described in our brief DOM foray.

At this point, after this method runs, our DOM nodes will have been re-arranged, but their position on the screen will have remained static. Cool! There’s only one step left…

Playing

Hah! We have done it; we have animated the unanimatable.

Image for post
Image for post

Extra Credit

  • We can very easily implement onStart/onFinish callbacks
  • We can do neat things like incrementally offsetting the duration of the transition, so that each animated element takes a little bit longer to finish (this makes animations feel more organic)
  • The full power of Javascript is made available to us; our imagination is the only limit.

Download the Module

When the articles prop changes, FlipMove will automagically transition its children to their new positions :)

It can be customized with a bunch of additional fields:

Play with it now:

Acknowledgements

  • Paul Lewis coined the term FLIP, and the ideas used here come from his fantastic blog post, FLIP your Animations.
  • Sacha Greif and Tom Coleman’s book, Discover Meteor, contains a chapter on animations, and they tackle this problem in a very similar way.

Developers Writing

Developers may not need to blog; but here your words are…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store