Build Real-Time Collaborative Cursors: Figma-Style Presence

Live cursors — those tiny avatars showing where other people are clicking — became table stakes for collaborative apps after Figma popularized them. The interview tests whether you understand the WebSocket plumbing, the throttling, the smooth-interpolation tricks, and the presence layer that powers them.

Functional requirements

  • Show other users’ cursor positions in real time
  • Each cursor has a name/avatar
  • Cursors disappear when users leave
  • Smooth movement, not jumpy
  • Scales to dozens of concurrent users on the same document

Architecture

WebSocket connection per client. Server broadcasts cursor positions to all clients on the same document.

For high-scale: each document is partitioned to a single server instance. WebSocket clients on that document connect to that instance directly, avoiding cross-instance routing.

Throttling

Mouse moves fire many events per second — too many to broadcast.

Client-side throttling:

  • Sample mouse position at 30Hz (every 33ms)
  • Only send if position changed by >1 pixel
  • Dispatch over WebSocket as a small binary message

Server-side throttling:

  • Optional: aggregate cursor updates and broadcast batches every 50ms instead of one-by-one
  • Reduces fanout cost at high concurrency

Smooth interpolation

The receiving client gets cursor updates at ~30Hz. To render at 60fps without stutter, interpolate:

  • Buffer the last 2 received positions
  • Tween between them using requestAnimationFrame
  • If new position arrives, smoothly transition to it

The result feels like 60fps movement even though server pushes at 30Hz.

Cursor metadata

Each cursor needs:

  • User ID (for color assignment)
  • Name (initial display)
  • Avatar (optional)
  • Color (deterministic from user ID, so it is stable across sessions)

Presence layer

Beyond cursors:

  • Active users list (sidebar, top bar)
  • “Who is editing this” tags on individual elements
  • Selection rectangles (for shape tools, highlight a user’s selection)

Presence is a separate channel from data updates. Don’t conflate.

Disappearing cursors

When a user disconnects:

  • Server detects WebSocket close
  • Broadcasts “user left” to others
  • Other clients fade their cursor with a 200ms transition

Edge case: brief network drops. Use a “last seen” timeout (30 seconds) before declaring a user gone.

Coordinate systems

The cursor position must translate between:

  • The sender’s viewport coordinates
  • The document’s logical coordinates
  • The receiver’s viewport (with their own pan/zoom)

Send logical coordinates over the wire. Each client transforms to their own viewport.

Bandwidth

Per-user bandwidth: ~30 messages/sec × 50 bytes = 1.5 KB/sec downlink per user. With 50 users on a document, each client receives 75 KB/sec — fine on broadband.

For mobile or constrained network, reduce update rate to 15Hz or only send when significant change.

Implementation libraries

  • Liveblocks: drop-in presence + cursors + collaboration. Most-used in 2026.
  • Yjs / PartyKit: CRDT-based; more flexible, more setup
  • Custom: for full control, build on raw WebSocket

Common mistakes

  • Sending mouse events on every browser event (60+ Hz)
  • Hard-coded colors that collide between users
  • No interpolation — cursors jump from frame to frame
  • Cursors persisting after users leave (zombie cursors)
  • Coordinate system mismatch between sender and receiver

Frequently Asked Questions

How do you handle a tab going inactive?

Browser throttles requestAnimationFrame. Document.visibilityState event tells you the tab is hidden. Pause sending updates; clear cursor on remote clients.

What is the latency budget?

Sub-100ms feels real-time. 200–300ms feels noticeable but acceptable. Above 500ms feels broken.

Why does Figma feel smoother than competitors?

Significant engineering on the cursor pipeline: aggressive throttling, smart interpolation, partition-aware connection management. Other apps cut corners and feel jittery.

Scroll to Top