Implementing Live Cursors in Mobile Applications
Live cursors — one of those UX elements that look simple but implementation requires balance between animation smoothness, actual traffic, and correct scaling with dozens of concurrent users.
On web solved via CSS animation and transform: translate(). On mobile — native Animated API or react-native-reanimated, UIView animation or Flutter AnimatedWidget. Network level — Y.js Awareness or custom WebSocket protocol.
Protocol: Awareness vs Custom Channel
Y.js Awareness Protocol — right choice if application already uses Y.js for content sync. Awareness holds ephemeral states: not persisted, don't enter change history, auto-deleted on user disconnect.
// Update own cursor position
provider.awareness.setLocalStateField('cursor', {
x: normalizedX, // in document coords, not screen
y: normalizedY,
timestamp: Date.now()
});
// Subscribe to other cursors change
provider.awareness.on('change', ({ updated }) => {
updated.forEach(clientId => {
if (clientId === provider.awareness.clientID) return;
const state = provider.awareness.getStates().get(clientId);
if (state?.cursor) {
updateRemoteCursor(clientId, state.cursor);
}
});
});
If Y.js not used — custom WebSocket channel with throttle 30ms (≈33fps) sufficient for smooth feel. More frequent updates don't give noticeable UX improvement but increase traffic.
Coordinate Normalization
Critical moment: cursor coordinates must be in document coordinate system, not screen. Different users have different zoom levels, screen sizes, scroll positions. Passing screen coordinates — cursors jump across screen instead of following real position.
For scrollable document: cursorX = (screenX + scrollX) / scale, cursorY = (screenY + scrollY) / scale. On receive back: displayX = documentX * scale - scrollX. On zoom change by receiver — cursor position auto-recalculates.
Interpolation: Smooth Movement with Network Latency
Raw positions from server — stepwise, especially at 100–200ms RTT. Need interpolation.
In React Native with react-native-reanimated:
const remoteCursorX = useSharedValue(0);
const remoteCursorY = useSharedValue(0);
// On receiving new position from server
const updateCursor = (x, y) => {
remoteCursorX.value = withSpring(x, { damping: 20, stiffness: 300 });
remoteCursorY.value = withSpring(y, { damping: 20, stiffness: 300 });
};
const animStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: remoteCursorX.value },
{ translateY: remoteCursorY.value },
]
}));
withSpring adds spring interpolation — cursor "chases" real position smoothly. Alternative: withTiming with duration: 80 — simpler, less "alive".
On Flutter: AnimationController + Tween<Offset> with CurvedAnimation(curve: Curves.easeOut).
On native iOS: UIViewPropertyAnimator with .interruptible option — allows interrupting and restarting animation on new positions without artifacts.
Scaling: 50+ Users
Large user count causes several problems:
Traffic. N users × 33fps × ~50 bytes = at 50 users ~82 KB/s just for cursor updates. Solution: server-side throttling (server doesn't relay updates faster than every 50ms per client) + disable cursors for users outside viewport.
Rendering. 50 animated views simultaneously on mobile — load. Use Canvas-based rendering instead of separate View for each cursor. Draw all cursors in one CustomPainter / SKCanvas / Canvas in single pass.
Identification. At 50 users, name under cursor unreadable. Show name only on hover/tap on cursor, rest of time — only colored dot with avatar.
Displaying Name and Avatar
User name next to cursor — classic UX. Implementation: floating label following cursor with small offset. Problem: moving near screen edge, label exits bounds. Need clamp — if cursor closer than X px to right edge, show label left of cursor.
Avatar instead of standard pointer — often better than colored arrow. 24px diameter round image, cached in memory.
Timeline
Live cursors as separate component (without full collaborative system) — 1–2 weeks per platform. In context of full collaborative application — one of first features after basic document sync.







