[ReactJS]

13 May 2020

-

6 min read time

FLIP animation in React

First, Last, Invert and Play - aka FLIP animations. It's a cool technique you can leverage to do smooth animations used in libraries like Framer, and enables animating CSS layouts like flex and grid.

image

By Korneliusz Caputa

image

Intro

I made a short video introducing the topic:

If you watched it, you can skip to the code section.

Animations bring an application to life. In my experience they are often underestimated as an integral part of a design. To illustrate to yourself the importance of purposeful transitions, consider that nothing in nature changes state discretely. Even lightning arcs and branches along a beautiful path dictated by conductivity, looking for the easiest route to connect the ground with the sky.

It's a delight to see the contents of an application behave as they would in the real world. Purposefully designed animations also help with usability. They reduce the cognitive load on the user, help avoid change blindness and guide focus to what's important

Problems and solutions

Not all transitions have been created equal. Using a transition on dimensional properties (width, height, margin, padding, top, left etc.) with CSS comes with a penalty, since the browser has to recalculate the layout on every frame. To avoid that, we pre-calculate the transition that needs to happen before the browser has a chance to paint and then let it run smoothly.

To achieve all this, we get the boundingClientRects of the element's initial and final state, apply a CSS transform to it and run the whole thing by applying a transition on the transform.

The steps

A short illustration of how the process goes:

First and Last

The First is the element before the transition and Last is where we want it to go.

image

We need to store the boundingClientRect of the element and resize and/or reposition it to the target location.

Invert

In this stage, we need to be after browser layout but before browser paint. That means getting the boundingClientRect of the element will give us the information about its final state. We use this to compute the differences as shown.

image

Play

Finally, we make the element move and let the browser paint.

image
The element movement can be achieved in multiple ways, e.g.:

The code

So how to implement this? I gave it a shot using React hooks.

To keep the code short, I focused on handling only a case of a list. You can have a look at the live demo.

image

The whole hook looks like this:

function NoLib({ items }: ViewProps) {
const listRef = React.createRef<HTMLUListElement>();

useFlip({
root: listRef,
invert,
play,
});

return (
<ul ref={listRef}>
{items.map((item) => (
<li data-key={item} key={item}>
{item}
</li>
))}
</ul>
);
}

const useFlip = ({ root, invert, play }: FlipProps) => {
const origins = useRef<{ [key: string]: ClientRect }>({});
let firstRun = useRef(true);

useLayoutEffect(() => {
if (root.current === null) return;
const list = root.current;
const children: HTMLElement[] = Array.protoype.slice.call(list.children);

for (const child of children) {
const key = child.dataset.key!;

const next = child.getBoundingClientRect();
if (!firstRun.current) {
if (key in origins.current) {
const previous = origins.current[key];
const delta = getDelta(previous, next);
if (!isZero(delta)) {
invert(delta, child);

requestAnimationFrame(() => {
play(child);
});
}
}
}
origins.current[child.dataset.key!] = next;
}

firstRun.current = false;
}, [root, invert, play]);
};

const invert = (delta: Rect, elem: HTMLElement) => {
elem.style.transform = `translate(${delta.left}px, ${delta.top}px)`;
elem.style.transition = `transform 0s`;
};

const play = (elem: HTMLElement) => {
elem.style.transform = ``;
elem.style.transition = `transform 300ms ease`;
};

const getDelta = (start: Rect, target: Rect) => ({
top: start.top - target.top,
left: start.left - target.left,
width: start.width / target.width,
height: start.height / target.height,
});

const isZero = (delta: Rect) =>
delta.left === 0 &&
delta.top === 0 &&
delta.width === 1 &&
delta.height === 1;

Let's walk through this.

First

Code responsible for initiating First parameters:

const origins = useRef<{ [key: string]: ClientRect }>({});

const list = root.current;
const children: HTMLElement[] = Array.protoype.slice.call(list.children);

for (const child of children) {
const key = child.dataset.key!;
const next = child.getBoundingClientRect();
origins.current[child.dataset.key!] = next;
}

I'm relying on the list elements having a data-key attribute for identification.

<li data-key={item} key={item}>
{item}
</li>

On the first run the origins is filled up with the positions of the list items in current render.

I'm employing useRef here to have origins be mutable, persist across renders and not cause changes in the component lifecycle.

From the documentation:

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

Last

Code responsible for figuring out the Last parameters:

for (const child of children) {
const next = child.getBoundingClientRect();
}

Invert

Code responsible for figuring out the inversion:

for (const child of children) {
const key = child.dataset.key!;
const next = child.getBoundingClientRect();

if (key in origins.current) {
const previous = origins.current[key];
const delta = getDelta(previous, next);
if (!isZero(delta)) {
invert(delta, child);
}
}
}

Stuff that figures out the differences:

const getDelta = (start: Rect, target: Rect) => ({
top: start.top - target.top,
left: start.left - target.left,
width: start.width / target.width,
height: start.height / target.height,
});

const isZero = (delta: Rect) =>
delta.left === 0 &&
delta.top === 0 &&
delta.width === 1 &&
delta.height === 1;

I wanted to show that the actual logic that performs the inversion can vary. For this particular example, I used CSS transform:

const invert = (delta: Rect, elem: HTMLElement) => {
elem.style.transform = `translate(${delta.left}px, ${delta.top}px)`;
elem.style.transition = `transform 0s`;
};

Play

The final piece of the puzzle is controlled by this code:

requestAnimationFrame(() => {
play(child);
});

Again, the play function is abstracted since there are multiple ways to go about it. I stuck to my guns with CSS transform because I used that in the invert step.

const play = (elem: HTMLElement) => {
elem.style.transform = ``;
elem.style.transition = `transform 300ms ease`;
};

Technicalities

requestAnimationFrame

Note the requestAnimationFrame call in the Play step - we need this so that the Invert applied to the DOM can be processed by the browser. This is necessary if you choose CSS transition as your means of animating.

In case of the Web Animations API or Anime.js, it's not needed. Moreover, you can then roll invert and play into a single function e.g.

const invertAndPlay = (delta: Rect, elem: HTMLElement) => {
elem.animate(
[
{
transform: `translate(${delta.left}px, ${delta.top}px)`,
},
{
transform: `none`,
},
],
{ duration: 300 }
);
};

useLayoutEffect

From the React hooks documentation:

The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.

You can see how this hook lets you access to the DOM in a perfect moment for the Invert step. In theory useEffect can be used here too, but it's unreliable, especially in situations when things are being added/removed from lists.

Avoiding animation on first render

This depends on the situation and is a matter of personal taste, but usually I don't like things to transition on first render. For me animation must enhance the experience and not slow you down from getting the information you want. An example to the contrary would be a landing page with a beautifully orchestrated opening sequence.

To avoid spurious flailing of things "in the morning" of my list, I used a ref that switches to false after the first processing run.

let firstRun = useRef(true);

useLayoutEffect(() => {
const children: HTMLElement[] = Array.protoype.slice.call(list.children);

for (const child of children) {
if (!firstRun.current) {
/* do the flips */
}
}

firstRun.current = false;
}, [root, invert, play]);

Calamity!!!

FLIP animation can be a finicky beast - everything is fine and dandy until it isn't.

image

There is a lot more things to take care of to make it working reliably, smoothly and under edge cases.

So either I describe it all here, doubling the size of this post, or...

Be smart and use tools

My tool of choice for dealing with FLIP in React is react-flip-toolkit. Years ago I worked with react-flip-move as well, but it's mostly focused on reordering lists. It's a no-go if you start resizing things to fit different content for example.

How does the example look implemented with react-flip-toolkit?

function FlipKit({ items }: ViewProps) {
return (
<Flipper flipKey={items.join()}>
<ul>
{items.map((item) => (
<Flipped key={item} flipId={item}>
<li>{item}</li>
</Flipped>
))}
</ul>
</Flipper>
);
}

Flipper and Flipped are the foundational components of react-flip-toolkit.

Flipper serves as your FLIP animation container. It needs a flipKey prop - make sure that it changes every time you want the Flipper contents to animate!

Flipped wraps the things you want to be animated. Here, the flipId is important. It is the link between the First and the Last, making sure the library knows how the transitions should look like.

image

The toolkit handles the edge cases I haven't considered in the pure hook solution. (it was a mix of problems, including re-rendering the list while animations were still going and using in-between positions as Firsts)

image

Closing thoughts

react-flip-toolkit packs a serious punch and covers pretty much anything you may need for FLIP animations in React. It also works with other libraries should you be interested.

Having FLIP in conjunction with other transitions and animations in your toolbelt, sky is the limit in terms of what you can create. Think UX on steroids, beautiful page transitions, morphing shapes, funky text effects etc.

Hope you have fun with it! :)

image

By Korneliusz Caputa

More from our Blog

Keep reading