skip to content

A card must animate from its slot in a grid to a full-width detail view, and animating its width, height, top and left is visibly janky. What is the FLIP technique, and how does it animate that layout change smoothly?

level: seniorimportance: should knowfreq 38%

answer

  1. First, Last, Invert, Play
  2. measure before, measure after
  3. jump to the end, then fake the start
  4. only a transform is interpolated
  5. scaling distorts text and radii

basics

~20 s

FLIP stands for First, Last, Invert, Play: measure the element's starting box, apply the final layout and measure again, apply a transform that visually puts it back at the start, then animate that transform away. The layout change happens once, up front, and only a transform is interpolated during the animation.

solid answer

~50 s

FLIP is a way to animate a layout change without animating layout properties. **First**: measure the element's box with `getBoundingClientRect()` before you change anything. **Last**: apply the final state — add the class, move the node, change the grid — and measure the box again. **Invert**: compute the delta between the two boxes and apply a `transform` (a translate plus a scale) that makes the element *look* like it is still in its original position. **Play**: animate that transform to `none`, with a transition or `Element.animate()`. The result is that the browser does its layout work once, at setup, and every frame after that only interpolates a transform. The costs are real: scaling distorts text, borders, shadows and `border-radius` unless you counter-scale children or animate a proxy element, the two measurements force layout at the start, and interruption mid-flight has to be handled explicitly by remeasuring from the current position.

code

javascript · 24 lines
javascript
function flip(element, mutate, duration = 300) {
  const first = element.getBoundingClientRect();

  mutate(); // apply the real end state: class, reparent, grid change

  const last = element.getBoundingClientRect();

  const dx = first.left - last.left;
  const dy = first.top - last.top;
  const sx = first.width / last.width;
  const sy = first.height / last.height;

  if (dx === 0 && dy === 0 && sx === 1 && sy === 1) return Promise.resolve();

  element.style.transformOrigin = 'top left';

  return element.animate(
    [
      { transform: `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})` },
      { transform: 'none' },
    ],
    { duration, easing: 'cubic-bezier(0.2, 0, 0, 1)' }
  ).finished;
}

go deeper

for a junior

Know what the letters stand for — First, Last, Invert, Play — and the core idea: apply the final layout immediately, then animate a transform that undoes the visual jump.

for a middle

Explain the mechanics: two getBoundingClientRect() measurements, computing translate and scale deltas, applying the transform before any paint, then animating it to none so only a transform is interpolated.

for a senior

Demonstrate the tradeoffs — scale distortion and the counter-scale or proxy remedies, batching reads and writes across many elements, handling interruption by remeasuring, and knowing when a crossfade is the better call.

for a principal

Decide whether hand-rolled FLIP belongs in the codebase at all: whether the animation library or the platform's view-transition support covers it, what the team is allowed to hand-write, and how motion complexity is kept from spreading across feature teams.

## The problem FLIP solves Some animations are *about* layout. A card grows into a detail view, an item flies from a list into a cart, a list reorders and every row slides to its new place. The naive implementation animates the properties that describe geometry — `width`, `height`, `top`, `left`, `margin` — and every frame of that animation asks the browser to redo layout for the affected part of the page and repaint it. On a small page you might get away with it; on a real one, with a grid of many cards, the per-frame cost blows straight through the frame budget and the animation stutters exactly when it is most visible. FLIP, named by Paul Lewis, restructures the problem: instead of animating *toward* the final layout, you jump to the final layout instantly and animate the *appearance* of the old one away. ## The four steps **F — First.** Before touching anything, record where the element is: ```js const first = el.getBoundingClientRect(); ``` **L — Last.** Apply the end state for real — add the class, append the node to a new parent, change the grid template. Then measure again: ```js el.classList.add('expanded'); const last = el.getBoundingClientRect(); ``` At this instant the element is already in its final position. The browser has done its layout work, once. **I — Invert.** Compute the transform that maps the final box back onto the original one and apply it, without a transition: ```js const dx = first.left - last.left; const dy = first.top - last.top; const sx = first.width / last.width; const sy = first.height / last.height; el.style.transformOrigin = 'top left'; el.style.transform = `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`; ``` The element is now laid out in its final place but *looks* like it never moved. **P — Play.** Animate the transform back to identity: ```js el.animate( [{ transform: el.style.transform }, { transform: 'none' }], { duration: 300, easing: 'cubic-bezier(0.2, 0, 0, 1)' } ).finished.then(() => { el.style.transform = ''; }); ``` Every frame between start and finish interpolates one transform. No layout property changes during the animation at all. ## Why this is faster The expensive work — recomputing the geometry of the page — happens exactly twice, both times during setup, rather than once per frame for the duration of the animation. `transform` and `opacity` are the two properties browsers can update most cheaply during an animation, which is why every serious motion system funnels its animations into them. FLIP is the general trick for expressing a layout change in that vocabulary. It also composes: to animate a whole list reordering, run First on every item, apply the new order once, then Last/Invert/Play each item. The list relayouts once no matter how many rows move. ## What it costs you FLIP is not free, and a senior answer names the costs. - **Scaling distorts.** A `scale()` stretches everything inside the element — text, padding, borders, `border-radius`, shadows. A card scaled 3× vertically shows fat, blurry text mid-flight. The standard remedies: counter-scale the inner content with the inverse scale (fiddly, and it must be animated in step), animate an empty proxy element and crossfade the real content over it, or avoid scale entirely by animating only position when the size change is small. - **You force layout twice.** Both `getBoundingClientRect()` calls make the browser compute geometry on the spot. Two of them at setup is a fine trade for a smooth animation, but batch them — measure all elements, then write all styles — rather than interleaving reads and writes per item. - **You must not paint between Invert and Play.** If the browser renders a frame after you have applied the final layout but before the inverting transform lands, the element flashes into its end position. Apply the invert in the same synchronous block as the measurement. - **Interruption is your problem.** If the user re-triggers the animation halfway, you cannot resume from a static assumption — you remeasure from the element's current visual position, which is what the current transform produced. Using `Element.animate()` helps here: `getAnimations()` finds the running animation, and its current state is inspectable. - **The DOM really did change at step L.** Focus, scroll position, and anything an observer reacts to move at the moment you apply the final layout, not when the animation ends. That is usually what you want, but it can surprise code that assumes the visual state matches the layout state. ## When to reach for it, and what else exists Use FLIP when the animation is genuinely a layout change and the naive version measurably stutters. Do not reach for it for a fade, a slide with a known offset, or anything already expressible as a transform — you would be adding measurement code for nothing. Animation libraries implement FLIP under the hood for shared-element and list-reorder transitions, so on a project that already has one, the honest answer is often "use the library's shared-layout animation, and know it is FLIP inside". The View Transition API is the platform's own take on the same problem — you mutate the DOM, the browser handles snapshotting and animating between the two states — and it removes most of this code where you can rely on it; browser support is still uneven, so check your targets rather than assuming. And sometimes the right senior answer is to not animate the layout change at all: crossfade between the two states, or animate a simpler stand-in. A 200 ms crossfade nobody notices beats a 400 ms morph that drops frames.

  • After a FLIP scale animation the card's text looks stretched mid-flight. What are your options?
    Scale is applied to everything inside the element, so the fix is to stop the content from inheriting it. Counter-scale the inner wrapper with the inverse factor, animated in step with the outer one; animate an empty proxy box and crossfade the real content in over it; or drop the scale entirely and animate only position, accepting a size pop if the difference is small.
  • How do you FLIP a list where ten items all move to new positions?
    Measure every item first, apply the new order once, then compute each item's delta and play each inverting transform. The key is batching: all reads, then the single DOM change, then all writes. The browser recomputes geometry once for the whole list regardless of how many items moved.
  • The element flashes into its final position for one frame before the animation starts. What went wrong?
    A frame was rendered between applying the final layout and applying the inverting transform. Both must happen in the same synchronous block — measure, apply the end state, measure again, set the transform, all before the browser gets a chance to paint. Awaiting a promise or scheduling the invert for later opens exactly that gap.
  • When would you not use FLIP even though the animation is a layout change?
    When the naive version already holds its frame budget — a single small element moving a short distance rarely needs it. When a crossfade reads just as well and costs a fraction of the code. When the framework or animation library already provides shared-layout transitions built on the same idea. Complexity you do not need is its own defect.

saying these in an interview costs you the question

  • Thinks FLIP animates width and height more efficiently
  • Applies the final layout and the invert in separate frames, causing a flash
  • Ignores that scale distorts text, borders and border-radius
  • Interleaves per-element measure and write instead of batching
  • Reaches for FLIP on animations that were never layout changes

context