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
| Prop | Type | Default | Description |
|---|---|---|---|
roomKey | (string | null)[] | (required) | Room key for cursor sync |
throttle | number | 16 | Milliseconds between updates (~60fps) |
renderCursor | (cursor: RemoteCursor) => ReactNode | Default SVG arrow | Custom cursor renderer |
defaultCursorStyle | CSSProperties | undefined | Styles for each cursor wrapper |
showLabels | boolean | true | Show user labels next to cursors |
labelKey | keyof ClientData | (client) => string | Auto-detect | Key or function for label text |
colorFn | (clientId: string) => string | Hash-based HSL | Generate cursor color from client ID |
zIndex | number | 9999 | Z-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
| Option | Type | Default | Description |
|---|---|---|---|
throttle | number | 16 | Milliseconds between updates |
normalize | boolean | true | Convert to 0-1 viewport coordinates |
hideOnLeave | boolean | true | Set active=false on mouse leave |
inactiveTimeout | number | 3000 | Milliseconds before removing inactive cursors |
autoTrack | boolean | false | Automatically track mouse movement |
Return value
| Field | Type | Description |
|---|---|---|
cursors | RemoteCursor[] | All cursors including yours |
remoteCursors | RemoteCursor[] | Only other users' cursors |
updatePosition | (pos: { x: number; y: number; active?: boolean }) => void | Update your cursor position |
clientId | string | Your 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
inactiveTimeoutmilliseconds.
RemoteCursor type
interface RemoteCursor<ClientData> {
clientId: string
user: Client<ClientData>
position: { x: number; y: number; active: boolean }
isYou: boolean
}