state-driven transitions → FLIP: animating position changes
React mutates the DOM; the browser paints. There is no animation layer in React
itself — you opt into smoothness with CSS. The simple case: toggle a class/style on
a state change and transition: background .3s fills the in-between
frames. But when an element changes position (a list
reorders, a card grows), CSS transitions are blind to it — React re-renders and the
box just snaps to its new slot. FLIP fixes that by
turning a position change into a transform, which the GPU animates cheaply.
| FLIP step | what you do | code |
|---|---|---|
| First | record the element's old box before the change | old = el.getBoundingClientRect() |
| Last | let React move the DOM to its new position (state update + re-render) | setItems(reordered) |
| Invert | compute dy = old.top − new.top; apply a transform so it looks unmoved |
el.style.transform = 'translateY(' + dy + 'px)' |
| Play | drop the transform with a transition — the box glides to its real spot |
transition: transform .3s; transform = '' |
The measuring happens in useLayoutEffect (runs after DOM mutation,
before paint) so the user never sees the snap — only the smooth glide. This is
the engine inside Framer Motion's layout prop and the View Transitions API.
1 · the animations you write (edit me)
2 · Babel compiles JSX → element tree
<App/> becomes React.createElement(App). Each
setItems() re-renders; useLayoutEffect then measures every
keyed <li> and runs First → Last → Invert → Play before paint.
// (hit "compile & render" to see Babel's output)
3 · live React (FLIP, proven)
Click the card to toggle a smooth color transition. Click ↑/↓ to reorder the list — each item glides to its new slot instead of snapping. The gold-check clicks ↑ on Beta automatically and asserts the DOM order becomes [Beta, Alpha, Gamma].
order: — · gold: 3 items, Alpha first → move-up Beta → [Beta, Alpha, Gamma]
intent → animation pattern
| intent | pattern | why |
|---|---|---|
| color / size / opacity change | style={{transition:'.3s'}} + toggle className on state |
state-driven — no measuring needed; browser fills frames |
| element moves / reorders | FLIP in useLayoutEffect (measure → invert → play) |
React re-render snaps; FLIP turns the move into an animated transform |
| mount / unmount animation | defer removal: keep node, add exit class, remove on transitionend |
React unmounts instantly — you must delay it to animate out |
| shared-element / crossfade | native document.startViewTransition() (see view_transitions) |
browser does FLIP for you across the whole page |
| physics / gestures / drag | Framer Motion motion + layout prop |
library wraps FLIP + spring physics + AnimatePresence exits |