LogoRobo.js

Cursors

Real-time cursor tracking with useSyncCursor and SyncCursors.

Track and render cursor positions across clients in real-time. The useSyncCursor hook handles position broadcasting and throttling, while the SyncCursors component provides a ready-made cursor overlay.

SyncCursors component

Drop-in component that renders all participant cursors automatically.

import { SyncCursors } from '@robojs/sync'

function Activity() {
  return (
    <div style={{ position: 'relative', width: '100vw', height: '100vh' }}>
      <SyncCursors roomKey={['room', roomId]} />
      {/* Your content */}
    </div>
  )
}
import { SyncCursors } from '@robojs/sync'

function Activity() {
  return (
    <div style={{ position: 'relative', width: '100vw', height: '100vh' }}>
      <SyncCursors roomKey={['room', roomId]} />
      {/* Your content */}
    </div>
  )
}

Props

PropTypeDefaultDescription
roomKey(string | null)[](required)Room key for cursor sync
throttlenumber16Milliseconds between updates (~60fps)
renderCursor(cursor: RemoteCursor) => ReactNodeDefault SVG arrowCustom cursor renderer
defaultCursorStyleCSSPropertiesundefinedStyles for each cursor wrapper
showLabelsbooleantrueShow user labels next to cursors
labelKeykeyof ClientData | (client) => stringAuto-detectKey or function for label text
colorFn(clientId: string) => stringHash-based HSLGenerate cursor color from client ID
zIndexnumber9999Z-index for cursor container

Custom cursor rendering

<SyncCursors
  roomKey={['room', roomId]}
  renderCursor={(cursor) => (
    <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
      <CustomArrowIcon color={cursor.user.data?.color} />
      <span>{cursor.user.data?.name}</span>
    </div>
  )}
/>
<SyncCursors
  roomKey={['room', roomId]}
  renderCursor={(cursor) => (
    <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
      <CustomArrowIcon color={cursor.user.data?.color} />
      <span>{cursor.user.data?.name}</span>
    </div>
  )}
/>

Label resolution

When labelKey is not provided, labels are resolved in order: data.name, data.username, data.displayName, then the first 6 characters of clientId.

useSyncCursor hook

For more control, use the hook directly. It gives you access to cursor data without rendering anything.

Signature

function useSyncCursor<ClientData = unknown>(
  key: (string | null)[],
  options?: CursorOptions
): CursorResult<ClientData>
function useSyncCursor(
  key: (string | null)[],
  options?: CursorOptions
): CursorResult<ClientData>

Options

OptionTypeDefaultDescription
throttlenumber16Milliseconds between updates
normalizebooleantrueConvert to 0-1 viewport coordinates
hideOnLeavebooleantrueSet active=false on mouse leave
inactiveTimeoutnumber3000Milliseconds before removing inactive cursors
autoTrackbooleanfalseAutomatically track mouse movement

Return value

FieldTypeDescription
cursorsRemoteCursor[]All cursors including yours
remoteCursorsRemoteCursor[]Only other users' cursors
updatePosition(pos: { x: number; y: number; active?: boolean }) => voidUpdate your cursor position
clientIdstringYour client ID

Auto-tracking mode

const { remoteCursors } = useSyncCursor(['room', roomId], { autoTrack: true })

return (
  <div>
{remoteCursors.map((cursor) => (
  <div
    key={cursor.clientId}
    style={{
      position: 'absolute',
      left: `${cursor.position.x * 100}%`,
      top: `${cursor.position.y * 100}%`
    }}
  >
    {cursor.user.data?.name}
  </div>
))}
  </div>
)
const { remoteCursors } = useSyncCursor(['room', roomId], { autoTrack: true })

return (
  <div>
{remoteCursors.map((cursor) => (
  <div
    key={cursor.clientId}
    style={{
      position: 'absolute',
      left: `${cursor.position.x * 100}%`,
      top: `${cursor.position.y * 100}%`
    }}
  >
    {cursor.user.data?.name}
  </div>
))}
  </div>
)

Manual tracking

const { remoteCursors, updatePosition } = useSyncCursor(['room', roomId])

useEffect(() => {
  const handleMove = (e: MouseEvent) => {
    updatePosition({
      x: e.clientX / window.innerWidth,
      y: e.clientY / window.innerHeight
    })
  }
  window.addEventListener('mousemove', handleMove)
  return () => window.removeEventListener('mousemove', handleMove)
}, [updatePosition])
const { remoteCursors, updatePosition } = useSyncCursor(['room', roomId])

useEffect(() => {
  const handleMove = (e) => {
    updatePosition({
      x: e.clientX / window.innerWidth,
      y: e.clientY / window.innerHeight
    })
  }
  window.addEventListener('mousemove', handleMove)
  return () => window.removeEventListener('mousemove', handleMove)
}, [updatePosition])

Performance

  • Cursor positions are sent as ephemeral broadcasts, not persisted state. Late joiners don't see existing cursor positions until the next movement.
  • Own cursor position is stored in a ref — no re-renders on your own movement.
  • Only remote cursor changes trigger React state updates.
  • Inactive cursors are automatically removed after inactiveTimeout milliseconds.

RemoteCursor type

interface RemoteCursor<ClientData> {
  clientId: string
  user: Client<ClientData>
  position: { x: number; y: number; active: boolean }
  isYou: boolean
}

Next steps

On this page