GodUIGodUI
129Follow on X

Anatomy of Live Cursors

Why multiplayer cursors chase a spring instead of teleporting to every network update, and how each one enters and leaves without a manual timer.

01/04One motion.div, two shapes
Pointer
SVG path, presence-color fill, white stroke
Name flag
mt-3 -ml-1, rounded-tl-sm, so the tail points at the pointer
tsx
<motion.div style={{ x, y }} className="absolute left-0 top-0 z-popover flex items-start"><svg viewBox="0 0 24 24" width="20" height="20" style={{ color }}>  <path d="M5 3l5.5 16 2.2-6.8L19.5 10z" fill="currentColor" stroke="white" strokeWidth={1.5} /></svg><span className="mt-3 -ml-1 rounded-[10px] rounded-tl-sm ...">{cursor.message ?? cursor.name}</span></motion.div>
01

One motion.div, two shapes

A cursor position arriving over a websocket is noisy: batched, occasionally late, never perfectly smooth. Write it straight to style.left and the cursor teleports every time a message lands. In LiveCursors, every incoming coordinate is a target for a spring, and the spring is what gets painted.

The pointer and the name flag are siblings inside one animated motion.div. Only the outer element's x/y ever moves, so the pointer and flag stay in a fixed relationship to each other. rounded-tl-sm on the flag is the same idea as the comment pin's rounded-bl-sm: leaving one corner square gives it a tail, here pointing back up at the cursor tip it's attached to.

02/04Raw updates vs a spring

raw · x.jump(cursor.x)

spring · useSpring(700, 40, 0.6)

Raw
position snaps, no interpolation between updates
Spring
chases the target, arrives with a touch of overshoot
tsx
const config = { stiffness: 700, damping: 40, mass: 0.6 };const x = useSpring(cursor.x, config);const y = useSpring(cursor.y, config);React.useEffect(() => {if (reduce) {  x.jump(cursor.x);  y.jump(cursor.y);} else {  x.set(cursor.x);  y.set(cursor.y);}}, [cursor.x, cursor.y, x, y, reduce]);
02

Raw updates vs a spring

useSpring holds a motion value that chases whatever you .set() it to instead of jumping there. Every new { x, y } prop retargets the same spring instead of restarting an animation, so a burst of fast updates (a teammate scribbling quickly) doesn't queue up a pile of separate tweens. The spring keeps re-aiming at the newest target. stiffness: 700 is high, tuned to feel almost immediate, but the lag never reaches zero. The small catch-up motion reads as a person moving rather than a value snapping between fixed points.

Reduced motion keeps the stiffness and spring as they are and swaps to .jump(), the same instant repositioning a raw update would have done. Someone who turned off motion wants no chase at all, and a faster chase would still move. Keeping the two paths as an explicit if rather than tuning one spring down to "fast enough" guarantees that reduced motion holds completely still.

03/04Enter and exit are the array, not a toggle
Enter
opacity 0 → 1, scale 0.5 → 1, 150ms
Exit
the mirror transition, then unmounts without a manual timer
tsx
<AnimatePresence>{cursors.map((cursor) => (  <CursorFlag key={cursor.id} cursor={cursor} hideName={hideNames} />))}</AnimatePresence>initial={{ opacity: 0, scale: 0.5 }}animate={{ opacity: 1, scale: 1 }}exit={{ opacity: 0, scale: 0.5 }}transition={{ duration: 0.15 }}const tick = () => {for (const peer of peers) {  peer.x += (peer.tx - peer.x) * 0.02;  peer.y += (peer.ty - peer.y) * 0.02;  if (Math.abs(peer.tx - peer.x) < 0.02) peer.tx = 0.1 + Math.random() * 0.8;}setCursors(peers.map((p) => ({ id: p.id, name: p.name, x: p.x * w, y: p.y * h })));raf = requestAnimationFrame(tick);};
03

Enter and exit are the array, not a toggle

There's no visible prop and no manual show/hide state anywhere in this component. AnimatePresence wraps a .map() over cursors, keyed by cursor.id. When the array a caller passes in gains or loses an entry (a peer joins, a peer's connection drops), React's own reconciliation triggers the initial or exit transition. The component ignores why the array changed and reacts only to the diff.

SimulatedCursors doesn't animate anything itself. It runs a plain random walk in a requestAnimationFrame loop (each peer eases 2% of the way toward a random target every frame, picking a new target once it arrives) and hands the result to LiveCursors as ordinary cursors prop updates. The spring above still does all the smoothing; the simulation only supplies noisy target coordinates, the same way a network feed would.

The rAF loop checks reduce once and returns immediately if motion is disabled. No peers move, so nothing gets interpolated and no frames are spent. The loop itself is cleaned up with cancelAnimationFrame on unmount, and each CursorFlag's springs are owned by Framer's own lifecycle: no cursor keeps a timer running after AnimatePresence finishes its exit and removes it from the tree.

04/04Result
04

The result

Each update retargets a spring instead of painting a value straight to style. Reduced motion swaps to instant positioning rather than a faster tween. Enter and exit follow the shape of the cursors array, with no cursor-specific show/hide logic.

Motion Score

Live CursorsSS: Compositor-only
SopacityFade / cross-fade
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →