Awareness
Implementing presence, cursors, and real-time user status with the Yjs awareness protocol.
Overview
Awareness provides ephemeral state synchronization—data that syncs in real-time but doesn’t persist: cursor positions, user presence, selections, typing indicators.
Basic Setup
const awareness = provider .awareness
awareness .setLocalStateField ('user' , {
name : 'Alice' ,
color : '#f783ac'
})
Setting Local State
// Set single field (preserves others)
awareness .setLocalStateField ('cursor' , { x : 100 , y : 200 })
// Replace entire state
awareness .setLocalState ({
user : { name : 'Alice' , color : '#f783ac' },
cursor : { x : 100 , y : 200 }
})
// Clear state (on disconnect)
awareness .setLocalState (null )
Observing Other Users
awareness .on ('change' , ({ added , updated , removed }) => {
console .log ('Joined:' , added )
console .log ('Updated:' , updated )
console .log ('Left:' , removed )
renderPresence ()
})
function renderPresence() {
awareness .getStates ().forEach ((state , clientId ) => {
if (clientId === yDoc .clientID ) return // Skip self
renderCursor (state .cursor , state .user , clientId )
})
}
Cursor Synchronization
// Throttle updates to avoid flooding
const updateCursor = throttle ((x : number , y : number ) => {
awareness .setLocalStateField ('cursor' , { x , y })
}, 50 )
function onMouseMove (e : MouseEvent ) {
updateCursor (e .clientX , e .clientY )
}
function onMouseLeave() {
awareness .setLocalStateField ('cursor' , null )
}
Text Editor Cursors
Use relative positions so cursors survive text edits:
function updateTextCursor (selection ) {
awareness .setLocalStateField ('cursor' , {
anchor : Y.createRelativePositionFromTypeIndex (yText , selection .anchor ),
head : Y.createRelativePositionFromTypeIndex (yText , selection .head ),
user : awareness.getLocalState ()? .user
})
}
Activity Status
let idleTimeout : NodeJS.Timeout
function setActive() {
awareness .setLocalStateField ('status' , 'active' )
clearTimeout (idleTimeout )
idleTimeout = setTimeout (() => {
awareness .setLocalStateField ('status' , 'idle' )
}, 60000 )
}
document.addEventListener ('mousemove' , setActive )
document.addEventListener ('visibilitychange' , () => {
awareness .setLocalStateField ('status' , document.hidden ? 'away' : 'active' )
})
Typing Indicator
let typingTimeout : NodeJS.Timeout
function onTextInput() {
awareness .setLocalStateField ('activity' , { type : 'typing' })
clearTimeout (typingTimeout )
typingTimeout = setTimeout (() => {
awareness .setLocalStateField ('activity' , null )
}, 2000 )
}
Cleanup
window.addEventListener ('beforeunload' , () => {
awareness .setLocalState (null )
})
Throttle updates: Max 10-20 cursor updates/second
Minimal state: Don’t put large objects in awareness
Conditional updates: Only send when position changes significantly:
if (Math.abs (e .clientX - lastX ) > 5 || Math.abs (e .clientY - lastY ) > 5 ) {
awareness .setLocalStateField ('cursor' , { x : e.clientX , y : e.clientY })
}
Common Mistakes
Forgetting to filter self:
// WRONG: renders own cursor
awareness .getStates ().forEach (state => renderCursor (state ))
// CORRECT
awareness .getStates ().forEach ((state , clientId ) => {
if (clientId !== yDoc .clientID ) renderCursor (state )
})
Not cleaning up removed users:
awareness .on ('change' , ({ removed }) => {
for (const clientId of removed ) {
removeCursor (clientId )
}
})
See Also