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 API | what it does | what 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 prop | library behaviour | by-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 |