Skip to content
Roomful is in public beta — install with the @beta tag. Share feedback →

Cursor Engine

Audience: users.

Cursors synchronize pointer position across peers.

const cursors = room.useCursors<{ tool: 'pen' | 'eraser' }>();
interface CursorEngine<TCursor extends CursorData = CursorData> {
mount(el: HTMLElement): void;
unmount(): void;
render(options?: CursorRenderOptions): void;
subscribe(cb: (positions: CursorPosition<TCursor>[]) => void): Unsubscribe;
getPositions(): CursorPosition<TCursor>[];
setPosition(position: Partial<CursorPosition<TCursor>>): void;
}
type CursorData = Record<string, unknown>;
type CursorPosition<TCursor extends CursorData = CursorData> = {
userId: string;
name: string;
color: string;
x: number;
y: number;
xAbsolute: number;
yAbsolute: number;
element?: string;
idle: boolean;
} & Partial<TCursor>;

Extra cursor fields are preserved across peer sync as long as they are serializable. Reserved runtime fields such as userId, name, color, x, y, xAbsolute, yAbsolute, element, and idle remain owned by the cursor engine and room transport layer.

cursors.render({
container: '#canvas',
style: 'default',
showName: true,
showIdle: false,
idleTimeout: 3000,
zIndex: 9999,
onMount: (element) => element.classList.add('peer-cursor'),
});

Built-in renderer styles ('default' | 'arrow' | 'dot' | 'pointer' | 'none'):

  • default: SVG arrow cursor with a name label.
  • arrow: SVG arrow cursor with a name label.
  • dot: compact dot marker with an optional label.
  • pointer: compact pointer marker with an optional label.
  • none: disables the built-in renderer while keeping cursor tracking active.
  • Unknown style strings fall back to default.

Render options:

  • idleTimeout (ms) overrides the idle threshold used by the renderer.
  • onMount(element) is called when each peer cursor element is first created, allowing custom decoration of the rendered node.

Renderer behavior:

  • Cursor nodes are absolutely positioned inside the render container.
  • showName === false hides the built-in name label.
  • showIdle === false hides idle peers until they move again.
  • showIdle !== false keeps idle peers rendered and marks them idle in the DOM.
  • Built-in cursor movement uses CSS transitions for smooth interpolation. CursorOptions.smoothing (default true) toggles this CSS-transition interpolation.
cursors.mount(document.getElementById('board') as HTMLElement);
cursors.subscribe((positions) => {
for (const pos of positions) {
const id = `cursor-${pos.userId}`;
let el = document.getElementById(id);
if (!el) {
el = document.createElement('div');
el.id = id;
document.getElementById('board')?.appendChild(el);
}
el.style.position = 'absolute';
el.style.left = `${pos.x * 100}%`;
el.style.top = `${pos.y * 100}%`;
el.textContent = pos.name;
}
});
  • Throttle high-frequency cursor updates.
  • Keep cursor payloads small.
  • Use awareness for semantic state, not pointer telemetry.