# Build a Simple FLIP Animation in React

7 min readThe FLIP technique allows for declarative and performant animations. In this article, we will look at a simple way to implement this method using React.

When building user interfaces, we should be able to do so declaratively, allowing us to easily express *what* we want, rather than *how* we get there.
CSS is declarative by nature, but we can’t performantly animate layout without causing reflow and repaints, which in turn causes a laggy (also known as janky) experience. While this should be true for layout animations, they are inherently tricky and require some magic to make them performant.

This is where FLIP (First Last Invert Play) animations come to the rescue! Coined by Paul Lewis, it’s a simple formula that allows us to performantly move boxes around using transforms, avoiding reflow and repaints.

## Getting started

To get started, we will define a simple `Box`

component that takes a `size`

prop that can be toggled between two states when clicked:

## Adding continuity

Currently we can move the box back and forth and make it larger or smaller, but this all happens immediately. Let’s introduce some continuity into our design and allow the user to see how the box transitions between each state:

1function Box({ size }) {2 return (3 <div4 style={{5 width: size,6 height: size,7 background: 'hotpink',8 transition: 'all 280ms cubic-bezier(.12,.66,.5,1)',9 }}10 />11 )12}

In a perfect world, we would be able to stop here and add a `transition`

, but we have a couple of issues though:

- Transitions for
`width`

and`height`

won’t happen on the GPU, which end up causing layout reflow resulting in jank. - We can’t animate properties like
`justifyContent`

used in our parent container resulting in the box still jumping immediately to its next state.

Enter FLIP animations.

## First, Last, Invert, Play

To perform a FLIP animation, we start by taking snapshots of the start and end positions (the *First* and *Last* portions of FLIP). We can do this in React by using a `ref`

and the `useLayoutEffect`

hook to store the element’s measurements every time the `size`

prop changes since we know the layout has changed at this point:

*Note we set transform-origin as well so our calculations are always from the top-left.*

1function Box({ size }) {2 const ref = React.useRef(null)3 const lastBounds = React.useRef(null)4 React.useLayoutEffect(() => {5 const bounds = ref.current.getBoundingClientRect()6 lastBounds.current = bounds7 }, [size])8 return (9 <div10 ref={ref}11 style={{12 width: size,13 height: size,14 background: 'hotpink',15 transformOrigin: 'top left',16 }}17 />18 )19}

Now that we have the *First* and *Last* snapshots we can move onto calculating the *Invert*. We’ll start by adding a small utility to help calculate the difference between our two bounding rectangles:

1function getInvertedTransform(startBounds, endBounds) {2 return {3 x: startBounds.x - endBounds.x,4 y: startBounds.y - endBounds.y,5 scaleX: startBounds.width / endBounds.width,6 scaleY: startBounds.height / endBounds.height,7 }8}

Now we can use this to calculate the inverse of where our box was previously:

1React.useLayoutEffect(() => {2 const bounds = ref.current.getBoundingClientRect()3 if (lastBounds.current) {4 const invertedTransform = getInvertedTransform(lastBounds.current, bounds)5 // now we have enough information to animate to the next position!6 }7 lastBounds.current = bounds8}, [size])

For our animation, we’ll be using Popmotion to power our FLIP animation:

1React.useLayoutEffect(() => {2 const bounds = ref.current.getBoundingClientRect()3 if (lastBounds.current) {4 const invertedTransform = getInvertedTransform(lastBounds.current, bounds)5 animate({6 from: invertedTransform,7 to: { x: 0, y: 0, scaleX: 1, scaleY: 1 },8 duration: 800,9 onUpdate: (transform) => {10 const { x, y, scaleX, scaleY } = transform11 const translate = `translate(${x}px, ${y}px)`12 const scale = `scale(${scaleX}, ${scaleY})`13 ref.current.style.transform = `${translate} ${scale}`14 },15 })16 }17 lastBounds.current = bounds18}, [size])

At this point, the box is rendered in its *Last* position. We then apply the *Invert* value to pull it back to the *First* state, mimicking the initial state’s appearance. When running the `animate`

function, we then *Play* the transition and animate the box back to its rendered area.

Whew, that’s a lot! Try changing the duration and open up the console to see how the animation is applied. Notice we don’t animate `width`

or `height`

and instead animate the performant `scale`

and `translate`

properties:

## Interruptible animations

We’re looking pretty good so far! Our box animates performantly back and forth, but if you happen to click too fast before the animation is complete, you’ll notice that the box randomly scales and jumps around before animating back to the desired state.

Since we aren’t working with a traditional layout, we need to keep track of our current transforms we’ve applied to calculate from the current position correctly.

We’ll introduce two more utility functions to help with this:

1function removeTransformFromBounds(bounds, transform) {2 return {3 width: bounds.width / transform.scaleX,4 height: bounds.height / transform.scaleY,5 top: bounds.top - transform.y,6 right: bounds.right - transform.x,7 bottom: bounds.bottom - transform.y,8 left: bounds.left - transform.x,9 x: bounds.x - transform.x,10 y: bounds.y - transform.y,11 }12}1314function applyTransformToBounds(bounds, transform) {15 return {16 width: bounds.width * transform.scaleX,17 height: bounds.height * transform.scaleY,18 top: bounds.top + transform.y,19 right: bounds.right + transform.x,20 bottom: bounds.bottom + transform.y,21 left: bounds.left + transform.x,22 x: bounds.x + transform.x,23 y: bounds.y + transform.y,24 }25}

These functions respectively remove or add transformations to our bounding rectangle. We still need a few other pieces of information, though, to calculate the position and size correctly.

We’ll store the current animation that is running as well as the last transform that was applied. Now when calculating our `bounds`

, we will remove the current transform that is being applied in order to get an accurate measurement for our final position. We also check before calculating the inverse if there is an animation currently running, and if so, apply that transformation:

1function Box({ size }) {2 const ref = React.useRef(null)3 const animation = React.useRef(null)4 const lastBounds = React.useRef(null)5 const lastTransform = React.useRef({6 x: 0,7 y: 0,8 scaleX: 1,9 scaleY: 1,10 })11 React.useLayoutEffect(() => {12 const bounds = removeTransformFromBounds(13 ref.current.getBoundingClientRect(),14 lastTransform.current15 )16 if (lastBounds.current) {17 const invertedTransform = getInvertedTransform(18 animation.current19 ? applyTransformToBounds(lastBounds.current, lastTransform.current)20 : lastBounds.current,21 bounds22 )23 if (animation.current) {24 animation.current.stop()25 }26 animation.current = animate({27 from: invertedTransform,28 to: { x: 0, y: 0, scaleX: 1, scaleY: 1 },29 duration: 800,30 onUpdate: (transform) => {31 const { x, y, scaleX, scaleY } = transform32 const translate = `translate(${x}px, ${y}px)`33 const scale = `scale(${scaleX}, ${scaleY})`34 ref.current.style.transform = `${translate} ${scale}`35 lastTransform.current = transform36 },37 onComplete: () => {38 animation.current = null39 },40 })41 }42 lastBounds.current = bounds43 }, [size])44 return (45 <div46 ref={ref}47 style={{48 width: size,49 height: size,50 background: 'hotpink',51 transformOrigin: 'top left',52 }}53 />54 )55}

Putting it all together, we now have interruptible, performant animations between both states of our box using any layout method we want. Try changing how the Flex alignment is applied or maybe use CSS Grid and see that the box will always animate correctly to its next state!

## Summary

This was a simplified look at how FLIP animations work and the nuances they introduce, like the complexity of merely interrupting the currently running animation. We also assume `justifyContent`

changes in the parent when the `size`

prop changes, in a real-world situation, we would need an event system to notify us of updates like this. Not to mention if we were to have included text in our UI, we would need to apply scale correction since the box is distorted during the transition. Ideally, a library like Framer Motion will handle these differences for you. Hopefully, one day, *the platform* can ship these optimizations so we can effortlessly build performant UI and focus on more critical tasks.

## Resources

Animating Layouts with the FLIP Technique

- animation
- react