Live Cursors
Sync cursor positions in real time so everyone in the room sees where others are pointing. This is the same experience you see in tools like Figma, Miro, Oasiz and multiplayer canvases.
Live cursors make collaboration feel alive. When someone moves their mouse, you instantly see it. When they stop, their cursor stays in place. When they leave, it disappears.
In this guide, you’ll learn how to build that experience using Playroom Kit’s shared state system.
Getting Started
Live cursors are common in modern collaborative apps. If you have never noticed them before, open a shared design file in Figma or a board in Miro. Move your mouse and watch how others can see it instantly.
We are going to recreate that same behavior using React and Playroom Kit.
This guide assumes:
- You already have a React app
- Playroom Kit is installed
- You want to add live cursors to your existing app
Don’t have an app yet? You can download the full example from GitHub, or create a fresh React app by following the React docs.
Vibe Coding System Prompt
Vibe-code friendly
If you are using an AI coding tool, copy this prompt:
You are a senior software engineer at Little Umbrella specializing in building real-time multiplayer applications using Playroom Kit and React.Your task is to help me build a "live cursors" feature similar to the liveblocks Live Cursors example.The application should be created using React as a frontend framework and Playroom Kit as a multiplayer framework.Do not assume the existence of any Playroom Kit APIs or hooks. Only use APIs documented in the official Playroom Kit documentation.I already have a React project set up.I will provide step-by-step instructions. After each step, clearly explain what was implemented and ask if I want customizations before proceeding.
You can adjust the prompt according to whether or not you have an existing application.
Guide
If you haven’t cloned the starter repository yet, clone it before moving to the first step. The starter code contains the basic React setup and Playroom Kit configuration you’ll need.
If you’re vibe-coding, start by copying the Vibe Coding System prompt. Then copy each step one by one into your coding assistant by clicking “Copy Prompt”.
Step 1: Setup Playroom Kit
First, make sure Playroom Kit is installed and configured. If you haven’t done that yet, follow the Setup guide.
Once that’s done, you need to connect to a room when your app starts. Use insertCoin() to join or create a session.
"use client";
import { useEffect } from "react";
import { insertCoin, myPlayer, usePlayersList } from "playroomkit";
import Cursor from "../components/Cursor";
import styles from "./index.module.css";
export default function Home() {
useEffect(() => {
insertCoin();
});
const players = usePlayersList(true);
const me = myPlayer();
return (
<main className={styles.container}>
<div className={styles.text}>Move your cursor to broadcast its position</div>
</main>
);
}Step 2: Get List of Players
Now that we are connected to a room, we need two things: a list of everyone in the room and a way to access ourselves. Playroom Kit provides both through usePlayersList and myPlayer.
"use client";
import { useEffect } from "react";
import { insertCoin, myPlayer, usePlayersList } from "playroomkit";
import Cursor from "../components/Cursor";
import styles from "./index.module.css";
type PlayerLite = { id: string; getState: (key: string) => any };
const COLORS = [
"#E57373",
"#9575CD",
"#4FC3F7",
"#81C784",
"#cbb913",
"#FF8A65",
"#F06292",
"#7986CB",
];
const colorForId = (id: string) =>
COLORS[id.split("").reduce((acc, ch) => acc + ch.charCodeAt(0), 0) % COLORS.length];
export default function Home() {
useEffect(() => {
insertCoin();
});
const players = usePlayersList(true) as PlayerLite[];
const me = myPlayer();
return (
<main className={styles.container}>
<div className={styles.text}>Move your cursor to broadcast its position</div>
</main>
);
}Step 3: Create Cursor Component
Create a simple Cursor component that renders an SVG cursor with the given color and position.
type Props = {
color: string;
x: number;
y: number;
};
export default function Cursor({ color, x, y }: Props) {
const SCALE = 2.5;
return (
<svg
style={{
position: "absolute",
left: 0,
top: 0,
transform: `translateX(${x}px) translateY(${y}px) scale(${SCALE})`,
transformOrigin: "0 0",
}}
width="24"
height="36"
viewBox="0 0 24 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19841L11.7841 12.3673H5.65376Z"
fill={color}
/>
</svg>
);
}Step 4: Send Your Cursor Position
Now we need to send our mouse position to everyone else. Every time the mouse moves, we store its position in our player state using setState with the third parameter set to false for fast, low-latency updates. This is explained in Reliable vs. Unreliable State Changes.
return (
<main
className={styles.container}
onPointerMove={(event) => {
if (me?.setState) me.setState("cursor", { x: Math.round(event.clientX), y: Math.round(event.clientY) }, false);
}}
onPointerLeave={() => {
if (me?.setState) me.setState("cursor", null, false);
}}
>
<div className={styles.text}>Move your cursor to broadcast its position</div>
</main>
);Step 5: Render Other Players’ Cursors
Now we render everyone else’s cursor. Each player stores their cursor position under their state using setState, and we use getState to retrieve their position and filter out ourselves before rendering.
const myCursor = players.find((p) => p.id === me?.id)?.getState("cursor") as
| { x: number; y: number }
| null
| undefined;
const statusText = !me
? "Waiting for Playroom to initialize..."
: myCursor
? `${myCursor.x} × ${myCursor.y}`
: "Move your cursor to broadcast its position to other people in the room.";
return (
<main
className={styles.container}
onPointerMove={(event) => {
if (me?.setState) me.setState("cursor", { x: Math.round(event.clientX), y: Math.round(event.clientY) }, false);
}}
onPointerLeave={() => {
if (me?.setState) me.setState("cursor", null, false);
}}
>
<div className={styles.text}>{statusText}</div>
{players
.filter((player) => player.id !== me?.id)
.map((player) => {
const playerCursor = player.getState("cursor");
if (!playerCursor) return null;
return <Cursor key={`cursor-${player.id}`} color={colorForId(player.id)} x={playerCursor.x} y={playerCursor.y} />;
})}
</main>
);Improvements
Once you have the basic live cursors working, here are some ways to make the experience even better:
- Add cursor interpolation – Use a library like Framer Motion or a simple easing function to smooth cursor movement between position updates
- Show player names – Display each player’s name next to their cursor so you know who is who
- Add different cursor shapes – Change the cursor icon based on what the user is doing (selecting, drawing, erasing)
- Implement cursor trails – Create subtle trails or effects for more visual feedback
- Add Lodash debounce – Reduce the frequency of updates when the mouse stops moving to optimize performance
FAQ
How do live cursors work under the hood?
Each player stores their cursor coordinates in their player state using setState with the unreliable transport flag. Other players subscribe to state changes through usePlayersList and render cursors when the data updates.
Why use unreliable transport for cursor positions?
Cursor positions update frequently (often 60+ times per second). Using Unreliable Transport ensures updates arrive as fast as possible without retransmission delays. If a packet is lost, the next update will arrive milliseconds later anyway.
Can I show cursors for users who aren’t moving?
Yes. The cursor stays at its last known position until the user moves again or leaves. When they leave, their cursor automatically disappears.
How do I limit how many cursors I render?
You can set a maximum by slicing the players array: players.slice(0, 10). This is useful for rooms with hundreds of participants.
Does this work on mobile devices?
Yes. Use onTouchMove instead of onPointerMove for mobile support. The same state management approach works across all platforms.
Can I customize cursor appearance per user?
Absolutely. You can store additional data in player state like cursor color, name, avatar, or tool type, then render different cursor styles based on that data.
How do I handle users leaving the room?
Playroom Kit automatically removes players from the usePlayersList array when they disconnect. Cursors for disconnected players will stop rendering automatically.
What’s the performance impact of live cursors?
Minimal. Each cursor is just a positioned SVG element. Even with 50+ cursors, modern browsers handle rendering easily. The network impact is also low because updates are small (just x and y coordinates) and sent unreliably.
Can I build this without React?
Yes. Playroom Kit works with any framework. The core insertCoin and player management APIs are framework-agnostic. You can use vanilla JavaScript, Vue, Svelte, or any other frontend library.
Is this the same approach Figma uses?
Figma uses a similar concept but with more optimizations like viewport culling (only rendering cursors visible in your area) and WebSocket compression. For most applications, the approach in this guide is more than enough.
Conclusion
You just built a complete live cursors system using Playroom Kit. This is the same real-time collaboration pattern used by Figma, Miro, and modern multiplayer applications.
The key takeaways:
- Use
myPlayerto access the current user - Use
usePlayersListto get everyone in the room - Store transient data like cursor positions with the unreliable transport flag for speed
- Render other players’ cursors by reading from their state
From here, you can extend this pattern to build shared whiteboards, collaborative document editing, multiplayer games, and any other real-time experience. Playroom Kit handles the hard parts so you can focus on building.