Step 6: See their cursors, live
A facepile with useDocumentPresence, and a real remote pointer moving over your own canvas.
Data converging is most of the way there. The last thing a spatial tool wants is to see who's there — a facepile in the header, exactly like Getting Started's text board got, using the same hook:
function PresenceStrip({ handle }) {
const others = useDocumentPresence(handle).filter((p) => !p.isSelf)
if (others.length === 0) return null
return (
<span style={{ display: "flex" }}>
{others.map((p) => (
<span key={p.userId} style={{ background: p.color /* … */ }}>
{p.name.slice(0, 1).toUpperCase()}
</span>
))}
</span>
)
}Pull handle out of useDocument and thread it into Board, next to the pin count.
Edit and the example re-runs. Tab indents; press Escape to leave the editor.
You should see: nothing changes yet — there's only one of you in this pane. Presence lives in the awareness channel, never your document, so it has nothing to show until someone else shows up.
Here's someone else showing up — two real identities, each with their own cursor, both looking at the same board:
Move your pointer inside a pane — the other one draws it, right over the board, not just
somewhere on the page. Drop a pin and the coloured chip appears in both headers, because
both panes are running your PresenceStrip, reading the same useDocumentPresence(handle).
That's the whole reason a spatial tool's presence reads differently from a text tool's: the cursor isn't decorating a line of prose, it's a second person's hand on the same board as yours.
Recap
useDocumentPresence(handle)lists everyone in the document right now — build a facepile from it with no channel of your own to manage.- Presence is ephemeral — it lives in the awareness channel, never your document — so a lone pane shows nothing until someone else arrives.
- The remote cursor you just watched move is real: a genuine second
Awareness, relayed live, never simulated.
What you built
A document-backed pin board: drop a pin at the exact pixel you clicked, drag one without
drift, mount the same component full-size or card-sized via context.view, and watch two
people work it at once — data and cursors both, live. Six steps, no sync code written by
hand.