FLIP – Performant Layout Transitions even with Left/Top/etc., with React Hooks!

FLIP – Performant Layout Transitions even with Left/Top/etc., with React Hooks!

FLIP is a way to animate elements on the page performantly even when changing properties like left, top, height, width, etc.

Here’s the experiment for you to take a look at: https://jayant.dev/experiments/flip-animation-technique
Note: We’ll do this in React, feat. some TypeScript.

If you took a look at a previous post I wrote, you know that the properties I mentioned above aren’t performant to animate, and result in janky motion anyway. It also mentions that sometimes these properties are your only hope. So is there no way we can do 60fps for such use cases?
Sure there is!

Presenting FLIP.
It stands for First – Last – Invert – Play.
Here’s what it means…

Get the current client rect for the element, so you know where the element is/was at the start.

Perform the action that results in the layout change to occur.

Get the client rect for the element after the intended action completed, so you know where it is/was at the end.

Make the element appear at the same place as it was at the beginning of this process. Effectively meaning that it’ll show up as it was during the First phase, and as far as the user cares nothing has happened yet and the element is still shown in the same place as it was during the First phase.

Calculate the offset between the First and the Last phases to know the X/Y position difference, and Scale difference.

Now that your element is being shown at the First phase position, and you know the offset between that and the Last phase, you can effectively just animate on the translate and scale properties. You’d translate on the positional difference, and scale on the proportional size difference.

Alright. That was the explanation of what FLIP means, and you’re around 300 words through, and we still don’t know how to do this. Let’s do this, in React.

We’ll use the latest and greatest in React to do this. Hooks.

Since we have to read properties from the layout after a layout change has been triggered, the useLayoutEffect makes the most sense.
We also need to get a reference to the element itself to read things from, for which we’ll use the useRef hook.

If it makes it easier for you to have the entire source before you continue, here you go: https://github.com/jayantbh/experiments/blob/master/src/components/FlipAnimationTechnique/index.tsx

We’ll work on a simple animation that changes the position and size of an element between two different states.

const before: CSSProperties = {
  height: '100px',
  width: '100px',
  left: '200px',

const after: CSSProperties = {
  height: '200px',
  width: '200px',
  left: '-200px',

And, that we’ll be applying one of these styles at all times based on a boolean like:

const elementRef = useRef(null as null | HTMLDivElement);
const [animated, setAnimated] = useState(false);
const toggleAnimation = useCallback(() => setAnimated(!animated), [animated]);
useAnimation(elementRef, animated);

  style={animated ? before : after}

Bit of an explanation on this since some stuff is new.
The useState hook gives you a state variable, and a setter for it.
The useCallback hook basically memoizes a function you pass to it, so you’re not creating a new function on each render. We use it to create a state setter function.
The useAnimation hook is our custom hook containing the logic that we’ll discuss below. Our logic requires the element reference (elementRef), and a boolean for whether the animation is toggled on or not (animated).

Now to make our animation work, we’ll create a custom hook (useAnimation). It’s not necessary to do so but it’ll keep the component code a bit cleaner. Remember that all code inside this hook can easily be put directly inside the component too. Let’s discuss what useAnimation will do.

After the first render, before any animation has been triggered by us, we get the element reference using our useRef hook. Subsequently, the useAnimation hook is fired once, but it does nothing because we will track the previously recorded client rect in our logic to use as the “First” phase rect, as such, our logic would know that there’s no previously recorded rect on the first run, and does nothing.

It’s also important to ensure that the useLayoutEffect hook doesn’t run again when we’re setting client rect in state, so we’ll track the previous value of animated property. If it remains the same we don’t want any animation logic to run again.

export const useAnimation = (elementRef: MutableRefObject<null | HTMLDivElement>, animated: boolean) => {
  // Maintain state of the element client rect
  const [clientRect, setClientRect] = useState(null as null | ClientRect);
  // Track whether the hook is being called with the same animation state.
  const { animated: prevAnimated } = usePrevious({ animated });

  // Run a side-effect to work in sync with layout changes because we'll be reading layout stuff
  useLayoutEffect(() => {
    // If this was fired with the same animation state as the last call, do nothing
    if (animated === prevAnimated) return;
    if (!elementRef || !elementRef.current) return;

    const el = elementRef.current;
    // Get first client rect from persisted state
    const first = clientRect;
    // Get current client rect from DOM
    const last = el.getBoundingClientRect();

    // Persist current client rect to state, so this is read at the "first" client rect next time

    // If either a persisted, or current client rect is missing, do nothing
    if (!first || !last) return;
    // ... rest of the code

So, on the first run, if (!first || !last) return; is going to terminate execution because first would be null. And animated will be the default value from useState.

The next time when the value of animated would have changed, we’ll have the clientRect persisted from the last time, and this forms what will be the “First” phase rect, and right after that we read the current element rect to get the “Last” phase rect.

That covers First, and Last.

The next time this hook, and hence the useLayoutEffect hook inside it will be fired is when the value of animated will change, which will have followed a layout change, making it safe for us to read any updated element layout properties BEFORE anything has been painted.

(You can run a LOT of JS before your browser paints anything, but you should avoid doing too much or you’ll end up dropping frames, or breaking the “illusion”. Run the el.animate in the full snippet within a setTimeout with a time of 16.7 or so and you’ll see what I mean.)

Then we’ll calculate the positional and scale differences in some way.

const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
const deltaW = first.width / last.width;
const deltaH = first.height / last.height;

The last two phases, Invert, and Play can be done with one API. The WebAnimations API.

// Use the WebAnimations API to make the element appear at the "first" phase by transforming it according to the offsets
      transformOrigin: 'top left',
      transform: `translate(${deltaX}px, ${deltaY}px) scale(${deltaW}, ${deltaH})`,
    // And then animate it to the "last", or current place by basically disabling the transformed offsets
      transformOrigin: 'top left',
      transform: 'none',
    duration: 250,
    easing: 'ease-in-out',

Element.animate accepts an array of Key-Frames.

We use the first key-frame to specify the “Invert” phase. We do that by transforming the element by translating it by the positional offsets that we calculated, and the scale differences that we calculated to make it appear the same size and in the same place at the “First” phase.

In the next Key-Frame, we basically turn off the transforms, making it animate into it’s actual position and size, triggering the “Play” phase, and concluding the animation. Obviously we specify an animation duration, and optionally an easing function.

This sums up what FLIP does. But it has a problem.
If there are any children that may need to be the same size and in the same place, they will be “raster-scaled” with the parent, making it look pretty odd.

How children will look like immediately after the animation is triggered to make the element smaller
How children will look like immediately after the animation is triggered to make the element smaller
How the children should look like immediately after the animation is triggered
How the children should look like immediately after the animation is triggered

This requires a pretty simple fix, at least in my experiment where the children are guaranteed to be centered, eliminating some position related problems. The fix we have to write will be for the scale.

The most common-sense approach to this that I could think of (and that some others also mention) is that animate the childrens scale inversely with the parent. So if the parent is going big-to-small based on some first/last parameter, we’ll animate the child based on a last/first parameter. Lo and behold, that is exactly what I’ve done.

const parentDeltaW = parentFirst.width / parentLast.width;
const parentDeltaH = parentFirst.height / parentLast.height;

// Get scale offset for the parent, because we want the child to scale inversely to the parent
const childDeltaW = parentLast.width / parentFirst.width;
const childDeltaH = parentLast.height / parentFirst.height;

Source for this, if you want to take a look: https://github.com/jayantbh/experiments/blob/master/src/components/FlipAnimationTechnique/hooks/use-child-aware-animation.ts#L33-L40

This concludes this experiment on FLIP animations.

This thing is crazy. This allows you to “fake” performant animations with left and all based property changes. This would make so many use-cases where you struggle to get a nice animation experience simply because the page has so much going on, easy to achieve well.

Here’s some stuff I read, and borrowed a lot from.



1 thought on “FLIP – Performant Layout Transitions even with Left/Top/etc., with React Hooks!”

Leave a Reply

Your email address will not be published. Required fields are marked *