With a very recent Safari update, Web Animations API (WAAPI) is now supported without a flag in all modern browsers (except IE). Here’s a handy Pen where you can check which features your browser supports. The WAAPI is a nice way to do animation (that needs to be done in JavaScript) because it’s native — meaning it requires no additional libraries to work. If you’re completely new to WAAPI, here’s a very good introduction by Dan Wilson.
One of the most efficient approaches to animation is FLIP. FLIP requires a bit of JavaScript to do its thing.
Let’s take a look at the intersection of using the WAAPI, FLIP, and integrating all that into React. But we’ll start without React first, then get to that.
FLIP and WAAPI
FLIP animations are made much easier by the WAAPI!
Quick refresher on FLIP: The big idea is that you position the element where you want it to end up first. Next, apply transforms to move it to the starting position. Then unapply those transforms.
Animating transforms is super efficient, thus FLIP is super efficient. Before WAAPI, we had to directly manipulate element’s styles to set transforms and wait for the next frame to unset/invert it:
// FLIP Before the WAAPI
el.style.transform = `translateY(200px)`;
requestAnimationFrame(() => {
el.style.transform = '';
});
A lot of libraries are built upon this approach. However, there are several problems with this:
- Everything feels like a huge hack.
- It is extremely difficult to reverse the FLIP animation. While CSS transforms are reversed “for free” once a class is removed, this is not the case here. Starting a new FLIP while a previous one is running can cause glitches. Reversing requires parsing a transform matrix with
getComputedStyles
and using it to calculate the current dimensions before setting a new animation. - Advanced animations are close to impossible. For example, to prevent distorting a scaled parent’s children, we need to have access to current scale value each frame. This can only be done by parsing the transform matrix.
- There’s lots of browser gotchas. For example, sometimes getting a FLIP animation to work flawlessly in Firefox requires calling
requestAnimationFrame
twice:
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.style.transform = '';
});
});
We get none of these problems when WAAPI is used. Reversing can be painlessly done with the reverse
function.The counter-scaling of children is also possible. And when there is a bug, it is easy to pinpoint the exact culprit since we’re only working with simple functions, like animate
and reverse
, rather than combing through things like the requestAnimationFrame
approach.
Here’s the outline of the WAAPI version:
el.classList.toggle('someclass');
const keyframes = /* Calculate the size/position diff */;
el.animate(keyframes, 2000);
FLIP and React
To understand how FLIP animations work in React, it is important to know how and, most importantly, why they work in plain JavaScript. Recall the anatomy of a FLIP animation:
Everything that has a purple background needs to happen before the “paint” step of rendering. Otherwise, we would see a flash of new styles for a moment which is not good. Things get a little bit more complicated in React since all DOM updates are done for us.
The magic of FLIP animations is that an element is transformed before the browser has a chance to paint. So how do we know the “before paint” moment in React?
Meet the useLayoutEffect
hook. If you even wondered what is for… this is it! Anything we pass in this callback happens synchronously after DOM updates but before paint. In other words, this is a great place to set up a FLIP!
Let us do something the FLIP technique is very good for: animating the DOM position. There’s nothing CSS can do if we want to animate how an element moves from one DOM position to another. (Imagine completing a task in a to-do list and moving it to the list of “completed” tasks like when you click on items in the Pen below.)
Let’s look at the simplest example. Clicking on any of the two squares in the following Pen makes them swap positions. Without FLIP, it would happen instantly.
There’s a lot going on there. Notice how all work happens inside lifecycle hook callbacks: useEffect
and useLayoutEffect
. What makes it a little bit confusing is that the timeline of our FLIP animation is not obvious from code alone since it happens across two React renders. Here’s the anatomy of a React FLIP animation to show the different order of operations:
Although useEffect
always runs after useLayoutEffect
and after browser paint, it is important that we cache the element’s position and size after the first render. We won’t get a chance to do it on second render because useLayoutEffect
is run after all DOM updates. But the procedure is essentially the same as with vanilla FLIP animations.
Caveats
Like most things, there are some caveats to consider when working with FLIP in React.
Keep it under 100ms
A FLIP animation is calculation. Calculation takes time and before you can show that smooth 60fps transform you need to do quite some work. People won’t notice a delay if it is under 100ms, so make sure everything is below that. The Performance tab in DevTools is a good place to check that.
Unnecessary renders
We can’t use useState for caching size, positions and animation objects because every setState
will cause an unnecessary render and slow down the app. It can even cause bugs in the worst of cases. Try using useRef
instead and think of it as an object that can be mutated without rendering anything.
Layout thrashing
Avoid repeatedly triggering browser layout. In the context of FLIP animations, that means avoid looping through elements and reading their position with getBoundingClientRect
, then immediately animating them with animate. Batch “reads” and “writes” whenever possible. This will allow for extremely smooth animations.
Animation canceling
Try randomly clicking on the squares in the earlier demo while they move, then again after they stop. You will see glitches. In real life, users will interact with elements while they move, so it’s worth making sure they are canceled, paused, and updated smoothly.
However, not all animations can be reversed with reverse
. Sometimes, we want them to stop and then move to a new position (like when randomly shuffling a list of elements). In this case, we need to:
- obtain a size/position of a moving element
- finish the current animation
- calculate the new size and position differences
- start a new animation
In React, this can be harder than it seems. I wasted a lot of time struggling with it. The current animation object must be cached. A good way to do it is to create a Map
so to get the animation by an ID. Then, we need to obtain the size and position of the moving element. There are two ways to do it:
- Use a function component: Simply loop through every animated element right in the body of the function and cache the current positions.
- Use a class component: Use the
getSnapshotBeforeUpdate
lifecycle method.
In fact, official React docs recommend using getSnapshotBeforeUpdate
“because there may be delays between the “render” phase lifecycles (like render
) and “commit” phase lifecycles (like getSnapshotBeforeUpdate
and componentDidUpdate
).” However, there is no hook counterpart of this method yet. I found that using the body of the function component is fine enough.
Don’t fight the browser
I’ve said it before, but avoid fighting the browser and try to make things happen the way the browser would do it. If we need to animate a simple size change, then consider whether CSS would suffice (e.g. transform: scale()
) . I’ve found that FLIP animations are used best where browsers really can’t help:
- Animating DOM position change (as we did above)
- Sharing layout animations
The second is a more complicated version of the first. There are two DOM elements that act and look as one changing its position (while another is unmounted/hidden). This tricks enables some cool animations. For example, this animation is made with a library I built called react-easy-flip
that uses this approach:
Libraries
There are quite a few libraries that make FLIP animations in React easier and abstract the boilerplate. Ones that are currently maintained actively include: react-flip-toolkit
and mine, react-easy-flip
.
If you do not mind something heavier but capable of more general animations, check out framer-motion
. It also does cool shared layout animations! There is a video digging into that library.
Resources and references
- Animating the Unanimatable by Josh W. Comeau
- Build performant expand & collapse animations by Paul Lewis and Stephen McGruer
- The Magic Inside Magic Motion by Matt Perry
- Using animate CSS variables from JavaScript, tweeted by @keyframers
- Inside look at modern web browser (part 3) by Mariko Kosaka
- Building a Complex UI Animation in React, Simply by Alex Holachek
- Animating Layouts with the FLIP Technique by David Khourshid
- Smooth animations with React Hooks, again by Kirill Vasiltsov
- Shared element transition with React Hooks by Jayant Bhawal
Source: CSS-tricks.com