Framer Motion Core — declarative animate, imperative rAF

[check: …]
📖 guide (.md) ← react deep dive
📖 Pair this live React playground with the companion guide (.md) — this page is the rendered ground truth. ↗ Compare with css_animations: CSS transitions animate CSS properties — Framer Motion orchestrates them with React state.

motion.div under the hood: the rAF tween loop

<motion.div animate={{ x: 80 }} /> looks like magic. It isn't. Motion stores the animate target, watches for changes, and on each change kicks off a requestAnimationFrame loop that writes el.style.transform every frame — bypassing React's render cycle entirely. React only re-renders when the target changes; the per-frame writes go straight to the DOM. Below we rebuild that loop by hand: read current value → tween with easeOut → write to style.

Framer Motion APIwhat it doeswhat we re-implement here
<motion.div animate> declarative target; library diff-and-tween on prop change useEffect([pos]) reads DOM, starts rAF loop
variants named animation states (hidden / visible) switchable by label a state value (open) selects the numeric target
whileHover / whileTap gesture targets applied on enter, reverted on leave onMouseEnter / onMouseLeave set style.transform
transition: {duration, ease} timing of the tween between values rafAnimate(from, to, 400, ...) + easeOutCubic

1 · the motion core you write (edit me)

2 · Babel compiles JSX → element tree

<MotionDemo/> becomes React.createElement(MotionDemo). Clicking a button bumps state → effect runs → rafAnimate starts the rAF loop → el.style is mutated every frame, never triggering a React re-render mid-animation.

// (hit "compile & render" to see Babel's output)

3 · live React (the motion core, proven)

Click "Move Right" — the box tweens to translateX(80px) over 400ms via rAF. Click "Open Panel" — height tweens from 0 to its content size. Hover the button — it scales up. The gold-check runs this automatically: assert box at 0 → click Move Right → wait for translateX(80px) → click Toggle → wait for panel height to grow.

box transform: · panel height: px · gold: moveRight(0→80px) → toggle(open) → assert height grew

Motion API → our re-implementation

motion proplibrary behaviourby-hand equivalent
initial first-frame values before enter animation initial style on the element
animate target to tween to; re-tweens on every change useEffect([deps])rafAnimate
exit tween before unmount (needs AnimatePresence) defer unmount: tween then call remove in onComplete
transition {type:'tween'} duration-based easing (the default) rafAnimate(from, to, ms, set) + easing fn
transition {type:'spring'} physics-based (stiffness / damping), no fixed duration rAF loop with velocity + spring force integration
variants named states; children inherit the active label state var picks the target object; pass label down as prop
whileHover / whileTap gesture target stacked on top of animate onMouseEnter / Leave set style
layout / layoutId FLIP: measure before/after, invert, play measure bbox, set transform, rAF to identity