Skip to main content
This guide walks you through building live cursor tracking where each user’s mouse position is displayed to all other participants in the room.

What You’ll Build

  • Real-time cursor positions for all connected users
  • User name labels on each cursor
  • Automatic cleanup when users disconnect
  • Different cursor colors per user

Prerequisites

  • A CollabKit server running and a room + user created (Quickstart)
  • @collab-kit/client and @collab-kit/utils installed

Step 1: Set Up the Client

import CollabKitClient from '@collab-kit/client';

const client = new CollabKitClient({
  serverUrl: 'https://api.collab-kit.com',
  authToken: '<jwt-token>',
});

await client.connect();
await client.join();

Step 2: Broadcast Cursor Position

Use the Presence module to share cursor coordinates. Updates are automatically throttled to 50ms:
document.addEventListener('mousemove', (e) => {
  client.presence.update({
    cursor: {
      x: e.clientX,
      y: e.clientY,
    },
  });
});

Step 3: Render Remote Cursors

Subscribe to all users’ presence and create/update DOM elements for each cursor:
const cursors = new Map<string, HTMLElement>();

// Generate a consistent color from a string
function stringToColor(str: string): string {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = str.charCodeAt(i) + ((hash << 5) - hash);
  }
  const hue = hash % 360;
  return `hsl(${hue}, 70%, 50%)`;
}

client.presence.sync('*', ({ userId, state }) => {
  // Skip our own cursor
  if (userId === client.userId) return;

  // User disconnected -- remove their cursor
  if (state === null) {
    cursors.get(userId)?.remove();
    cursors.delete(userId);
    return;
  }

  // Create or get the cursor element
  let el = cursors.get(userId);
  if (!el) {
    el = document.createElement('div');
    el.className = 'remote-cursor';
    el.style.position = 'fixed';
    el.style.pointerEvents = 'none';
    el.style.zIndex = '9999';
    el.style.transition = 'transform 0.1s ease-out';

    // Cursor dot
    const dot = document.createElement('div');
    dot.style.width = '8px';
    dot.style.height = '8px';
    dot.style.borderRadius = '50%';
    dot.style.backgroundColor = stringToColor(userId);
    el.appendChild(dot);

    // Name label
    const label = document.createElement('span');
    const user = client.users.all.get(userId);
    label.textContent = user?.name ?? userId;
    label.style.fontSize = '12px';
    label.style.marginLeft = '8px';
    label.style.backgroundColor = stringToColor(userId);
    label.style.color = 'white';
    label.style.padding = '2px 6px';
    label.style.borderRadius = '4px';
    label.style.whiteSpace = 'nowrap';
    el.appendChild(label);

    document.body.appendChild(el);
    cursors.set(userId, el);
  }

  // Update position
  el.style.transform = `translate(${state.cursor.x}px, ${state.cursor.y}px)`;
});

Step 4: Clean Up on Disconnect

Remove all cursor elements when the user leaves:
window.addEventListener('beforeunload', () => {
  void client.disconnect();
});
Other clients will receive a null state in the presence callback, which triggers cursor removal (handled in Step 3).

Step 5: Add Typing Indicators (Optional)

Extend the presence state to include a cursor mode:
// Track typing state
const input = document.querySelector('input');

input?.addEventListener('focus', () => {
  client.presence.update({
    cursor: { x: 0, y: 0, mode: 'typing' },
  });
});

input?.addEventListener('blur', () => {
  client.presence.update({
    cursor: { x: 0, y: 0, mode: 'idle' },
  });
});

// In the presence sync callback, check the mode:
client.presence.sync('*', ({ userId, state }) => {
  if (!state || userId === client.userId) return;

  if (state.cursor.mode === 'typing') {
    showTypingIndicator(userId);
  } else {
    hideTypingIndicator(userId);
  }
});

Next Steps

  • Combine cursor tracking with Follow Mode to let users follow each other
  • Add cursor click animations using Broadcasts
  • Track scroll position to sync viewports across users