Skip to main content
The Presence module lets users share short-lived, ephemeral state with other participants. Common use cases include live cursor tracking, text selections, scroll positions, and typing indicators. Access it via client.presence.
Presence data is not persisted — it exists only while users are connected.

Methods

Update Presence

update() Broadcast your current presence state to other participants. The input is a freeform object — you can include any data you want.
client.presence.update({
  cursor: { x: 100, y: 200 },
  screen: { width: 1920, height: 1080 },
});
Updates are throttled at 50ms to prevent flooding the WebSocket. If you call update() more frequently, the latest state is sent after the throttle window.
// Track cursor movement -- throttled automatically
document.addEventListener('mousemove', (e) => {
  client.presence.update({
    cursor: { x: e.clientX, y: e.clientY, state: 'idle' },
  });
});

// Track typing state
document.querySelector('input')?.addEventListener('keydown', () => {
  client.presence.update({
    cursor: { x: 0, y: 0, state: 'typing' },
  });
});

Subscribe to Presence Updates

sync(target, callback) Subscribe to presence changes from other users. The target parameter controls which users you receive updates from:
TargetPayloadDescription
'*'{userId: string, state: PresenceState}All users in the room
'following'{userId: string, state: PresenceState}Only users you are following
userId (string){userId: string, state: PresenceState}A specific user by ID
// Subscribe to all users
client.presence.sync('*', ({ userId, state }) => {
  if (state === null) {
    // User left or presence cleared, do something
    return;
  }
  // Update cursor position...
});

Unsubscribe from Presence Updates

unsync() Unsubscribe from presence updates:
client.presence.unsync();

Get All States

getStates() Get all current presence states as a map:
const states = client.presence.getStates();
// Map<string, PresenceState>

for (const [userId, state] of states) {
  console.log(`${userId}:`, state);
}

Get State

getState(userId?: string) Get a specific user’s presence state, or the current user’s state if no ID is provided:
// Get my own presence state
const myState = client.presence.getState();

// Get another user's presence state
const theirState = client.presence.getState('user-002');

Examples

Live Cursors

const cursors = new Map<string, HTMLElement>();

// Create or update a cursor element for each user
client.presence.sync('*', ({ userId, state }) => {
  if (userId === client.userId) return; // Skip self

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

  let el = cursors.get(userId);
  if (!el) {
    el = document.createElement('div');
    el.className = 'remote-cursor';
    document.body.appendChild(el);
    cursors.set(userId, el);
  }

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

// Broadcast local cursor
document.addEventListener('mousemove', (e) => {
  client.presence.update({
    cursor: { x: e.clientX, y: e.clientY },
  });
});

Follow Users

Use the 'following' sync target combined with the follow/unfollow user methods to build a “follow mode” where one user’s viewport mirrors another’s:
// Follow a user
const targetUser = client.users.all.get('user-002');
await targetUser.follow();

// Subscribe to followed user's presence
const cursorEl = document.createElement('div');
cursorEl.className = 'follow-cursor';
document.body.appendChild(cursorEl);

client.presence.sync('following', ({ userId, state }) => {
  if (state === null) {
    cursorEl.style.display = 'none';
    return;
  }

  const user = client.users.all.get(userId);
  cursorEl.style.display = 'block';
  cursorEl.style.left = `${state.cursor.x}px`;
  cursorEl.style.top = `${state.cursor.y}px`;
  cursorEl.title = user?.name ?? userId;
});

// Unfollow when done
await targetUser.unfollow();