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:
| Target | Payload | Description |
|---|
'*' | {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();