Skip to content

Presence & identity

Presence is ephemeral per-user state: it clears when someone leaves and does not replay for late joiners. Use persistent data when values should survive a reload.

For usage examples, see Presence. For cursor rendering and CursorOptions, see Cursors and init options.


Type: object
Persisted: yes (in localStorage per browser; not synced across devices)

interface PlayerIdentity {
  publicKey: string;
  name?: string;
  playerStyle: {
    colorPalette: string[];
    cursorStyle?: string;
  };
  discoveredSites?: string[];
  createdAt?: number;
}
FieldNotes
publicKeyStable participant id for this browser.
nameDisplay name, if set.
playerStyle.colorPaletteCursor colors. Index 0 is the primary color.
playerStyle.cursorStyleOptional custom cursor CSS.

Read your identity with playhtml.presence.getMyIdentity() (vanilla) or usePlayContext().getMyPlayerIdentity() / usePlayerIdentity() (React).

When the “we were online” browser extension is installed, it can inject identity via the playhtml:configure-identity DOM event. playhtml merges the extension’s color and public key automatically.


Vanilla: playhtml.presence (after playhtml.ready).
React: usePresence, usePresenceRoom.

interface PresenceAPI {
  setMyPresence(channel: string, data: unknown): void;
  getPresences(): Map<string, PresenceView>;
  onPresenceChange(
    channel: string,
    callback: (presences: Map<string, PresenceView>) => void,
  ): () => void;
  getMyIdentity(): PlayerIdentity;
}

Broadcasts a value on a named channel for your user. Pass null to clear the channel.

Replace semantics — each call overwrites the previous value for that channel. No partial merge.

playhtml.presence.setMyPresence("status", { text: "focused" });
playhtml.presence.setMyPresence("status", null);

Channel names become top-level keys on PresenceView. Do not collide with system fields (playerIdentity, cursor, isMe) — collisions are dropped.

Returns a snapshot Map keyed by connection id. Each value is a PresenceView:

type PresenceView<T = Record<string, unknown>> = {
  playerIdentity?: PlayerIdentity;
  cursor: Cursor | null;
  isMe: boolean;
} & T;

Custom channel data appears under the channel name (e.g. p.status for channel "status").

Subscribes to changes on one channel. Returns an unsubscribe function. Cursor movement uses channel "cursor".

Returns the local PlayerIdentity object.


Vanilla: playhtml.createPresenceRoom(name){ presence, destroy }
React: usePresenceRoom(name)

A separate presence scope from the page room (lobby, sidebar, shared document). The presence object has the same PresenceAPI shape. Call destroy() when done to disconnect and clear your presence for others.

const room = playhtml.createPresenceRoom("lobby");
room.presence.setMyPresence("status", { text: "ready" });
room.destroy();

Presence scoped to one playhtml element. Same lifetime as page presence — ephemeral, no replay.

Set on the Element API:

Property / callback fieldRole
myDefaultAwarenessYour starting awareness value.
awarenessRead-only array of every user’s value in callbacks.
setMyAwarenessBroadcast your value.
updateElementAwarenessImperative hook when awareness changes.

Built-in example: can-hover uses awareness { hover: boolean } and sets [data-playhtml-hover] when anyone hovers.

In React, withSharedState / <CanPlayElement> expose the same fields on render props: awareness, myAwareness, setMyAwareness.


When cursors: { enabled: true } is set at init, cursor position and identity are broadcast as presence on channel "cursor".

Types:

type Cursor = {
  x: number;
  y: number;
  pointer: "mouse" | "touch" | string;
};

type CursorPresenceView = {
  cursor: Cursor | null;
  playerIdentity?: PlayerIdentity;
  zone?: CursorZonePosition | null;
  page?: string;
};

type CursorZonePosition = {
  zoneId: string;   // matches element.id
  relX: number;     // 0–1 within the zone
  relY: number;
};

React hooks: useCursorPresences, useCursorZone, usePlayerIdentity.

Vanilla: playhtml.cursorClient (advanced; null when cursors are disabled). See playhtml client.

Full cursor config (room, container, proximityThreshold, chat, custom render): Cursors and cursors init option.