Skip to content

playhtml client

The playhtml object is the library entry point. Import it from the package, or use window.playhtml after a script tag loads.

// ES module
import { playhtml } from "playhtml";

// Script tag (CDN) — available as window.playhtml after the script loads
// <script type="module" src="https://unpkg.com/playhtml/dist/index.js"></script>
playhtml.init();

API groups at a glance:

GroupMembers
Lifecycleinit, configure, ready, isLoading, handleNavigation
Element setup & teardownsetupPlayElements, setupPlayElement, setupPlayElementForTag, removePlayElement, deleteElementData
Custom elements (experimental)register, define, getHandle
EventsdispatchPlayEvent, registerPlayEventListener, removePlayEventListener
Page datacreatePageData
Presencepresence, createPresenceRoom, cursorClient
InspectionroomId, host, syncedStore, elementHandlers, eventHandlers, listSharedElements

Signature: init(options?: InitOptions): Promise<void>

Connects to the room and activates playhtml elements. Resolves when initial sync finishes (same as await playhtml.ready).

import { playhtml } from "playhtml";

await playhtml.init({ room: "my-room" });

Safe to call more than once. Later calls reuse the connection. Options must match the first call; conflicting options warn and are ignored.

See init() options for all options.


Signature: configure(options?: InitOptions): void

Records options without connecting. Use when you cannot call a single init() at the top of the app (islands, multi-page sites, multiple roots).

// In a script that runs early (e.g. in <head>):
playhtml.configure({ cursors: { enabled: true } });

// Anywhere later — these just ensure playhtml is running:
playhtml.init();

configure() only records options; connection does not start until init() is called. If init() has already started, configure() behaves like a late init() call: matching options are accepted, conflicting options warn.

See init() options for all available options.


Type: Promise<void> (read-only)

Resolves when the initial sync is complete and elements are active. Rejects if the connection fails.

playhtml.init();

// Await sync before reading shared state
await playhtml.ready;
console.log("synced:", playhtml.roomId);

Type: boolean (read-only)

true from the time init() is called until the initial sync completes; false thereafter. Useful for showing a loading indicator.

if (playhtml.isLoading) {
  document.body.classList.add("loading");
}
playhtml.ready.then(() => {
  document.body.classList.remove("loading");
});

Signature: handleNavigation(): Promise<void>

Re-derives the room from the URL (or your room function), tears down the old room, and re-scans the DOM. Call after client-side navigation.

router.on("navigate", () => playhtml.handleNavigation());

See Navigation & SPAs for a full guide and framework-specific examples.


Signature: setupPlayElement(element: Element, options?: { ignoreIfAlreadySetup?: boolean }): void

Registers one element after init(). Use for elements added dynamically.

Pass { ignoreIfAlreadySetup: true } to skip elements that are already registered.

The element needs a unique id.

const card = document.createElement("div");
card.id = "card-1";
card.setAttribute("can-move", "");
document.body.appendChild(card);
playhtml.setupPlayElement(card);

Signature: setupPlayElements(): void

Scans the entire document and registers every element that carries a playhtml capability attribute. Called internally at the end of init(). Useful if you inject a batch of playhtml elements into the DOM at once and want to activate them all in one pass.

container.innerHTML = serverRenderedHtml;
playhtml.setupPlayElements();

Signature: setupPlayElementForTag(element: HTMLElement, tag: string): Promise<void>

Registers an element for a single specific capability tag. Lower-level than setupPlayElement; only needed when you want to activate one capability on an element that may carry others you do not want to activate yet.

await playhtml.setupPlayElementForTag(el, "can-move");

Signature: removePlayElement(element: Element | null): void

Detaches the element’s handler (listeners, timers, observers) but keeps shared data. Use when the DOM node is removed but may come back.

To delete shared data too, use deleteElementData.

playhtml.removePlayElement(document.getElementById("card-1"));

Signature: deleteElementData(tag: string, elementId: string): void

Deletes shared data for tag + elementId. Propagates to all clients. Also removes the local handler.

playhtml.deleteElementData("can-duplicate", "clone-abc123");

Throws a console warning if called before init() completes sync.


These three methods are part of the experimental view API. See View API.

Signature: register<T, U, V>(elementId: string, init: ElementInitializer<T, U, V>): PlayElementHandle<T, U, V>

Binds an initializer to one element by id. Returns a handle for reads and writes from outside the element’s own callbacks. Callable before or after init() and before or after the element exists in the DOM.

const handle = playhtml.register("my-counter", {
  defaultData: { count: 0 },
  view: ({ data, setData }) => html`
    <button @click=${() => setData(d => { d.count++ })}>
      Clicked ${data.count} times
    </button>
  `,
});

Signature: define<T, U, V>(capabilityName: string, init: ElementInitializer<T, U, V>): void

Registers a reusable capability under an attribute name. Every element carrying [capabilityName] binds, including elements added to the DOM later. The runtime equivalent of init({ extraCapabilities }).

playhtml.define("can-note", {
  defaultData: { text: "" },
  view: ({ data, setData }) => html`
    <textarea @input=${(e) => setData({ text: e.target.value })}>${data.text}</textarea>
  `,
});

Throws if capabilityName collides with a built-in capability name.


Signature: getHandle(elementId: string, capability?: string): PlayElementHandle

Returns a handle for any bound element. When one element carries multiple capabilities, pass the capability name (e.g. "can-toggle") to disambiguate.

const handle = playhtml.getHandle("card-1", "can-move");
console.log(handle.getData()); // { x: 120, y: 45 }

Events let you broadcast custom actions across all connected clients in the room. See Events for the full guide.

Signature: registerPlayEventListener(type: string, event: Omit<PlayEvent, "type">): string

Registers a listener for a custom event type. Returns a unique string id that you can pass to removePlayEventListener to clean up later.

// PlayEvent shape:
interface PlayEvent<T = any> {
  type: string;
  onEvent: (eventPayload: { eventPayload: T }) => void;
}
const listenerId = playhtml.registerPlayEventListener("confetti", {
  onEvent: () => window.confetti({ particleCount: 100 }),
});

Multiple listeners for the same type are allowed — each gets its own id.


Signature: dispatchPlayEvent(message: EventMessage): void

Broadcasts an event to all connected clients, including the sender. The type must already be registered via registerPlayEventListener (or the events init option).

// EventMessage shape:
type EventMessage<T = any> = { type: string; eventPayload?: T };
playhtml.dispatchPlayEvent({ type: "confetti" });
// With a payload:
playhtml.dispatchPlayEvent({ type: "highlight", eventPayload: { elementId: "card-1" } });

Signature: removePlayEventListener(type: string, id: string): void

Removes the listener identified by type and the id returned from registerPlayEventListener.

playhtml.removePlayEventListener("confetti", listenerId);

Page data lets you read and write named shared values that are not tied to any DOM element — good for page-wide settings, counters, or leaderboards.

See Page data for a full guide.

Signature: createPageData<T>(name: string, defaultValue: T): PageDataChannel<T>

Creates (or re-opens) a named shared data channel in the current room. Must be called after playhtml.ready resolves.

await playhtml.ready;

const views = playhtml.createPageData("viewCount", 0);
views.setData((n) => n + 1);
views.onUpdate((n) => {
  document.getElementById("views").textContent = String(n);
});

PageDataChannel<T> methods:

MethodDescription
getData(): TReturns the current value.
setData(data: T | ((draft: T) => void)): voidUpdates the value. Pass a mutator function for nested objects.
onUpdate(callback: (data: T) => void): () => voidSubscribes to changes. Returns an unsubscribe function.
destroy(): voidTears down the channel and removes all subscriptions.

Throws if called before init() completes sync.


Presence tracks who is in the room and what they are doing. See Presence & identity for types and API details. Usage guide: Presence.

Type: PresenceAPI

The main presence object for the current room. Throws if accessed before init() completes.

await playhtml.ready;

const presences = playhtml.presence.getPresences();
console.log(`${presences.size} people in this room`);

PresenceAPI methods:

MethodDescription
setMyPresence(channel: string, data: unknown): voidBroadcasts a presence value on the given channel to other clients.
getPresences(): Map<string, PresenceView>Returns a snapshot map of all currently visible presences, keyed by connection id. Each entry includes cursor, isMe, and playerIdentity alongside any data you set.
onPresenceChange(channel: string, callback: (presences: Map<string, PresenceView>) => void): () => voidSubscribes to presence changes on the given channel. Returns an unsubscribe function.
getMyIdentity(): PlayerIdentityReturns the local player’s persistent identity object.

Signature: createPresenceRoom(name: string): PresenceRoom

Creates a separate presence scope (sidebar, nested panel). Call destroy() when done.

await playhtml.ready;

const sidebar = playhtml.createPresenceRoom("sidebar");
sidebar.presence.setMyPresence("focus", { section: "comments" });

// Tear down when done
sidebar.destroy();

PresenceRoom shape:

interface PresenceRoom {
  presence: PresenceAPI;
  destroy(): void;
}

Throws if called before init() completes sync.


Type: CursorClientAwareness | null

Low-level cursor client, or null if cursors are disabled. Most config belongs in init({ cursors: … }). See Cursors.


Read-only debug surfaces. Do not mutate them directly.

Type: string (read-only)

The normalised room id the client is currently connected to (hostname-prefixed). Empty string before init().

console.log(playhtml.roomId); // e.g. "example.com-/my-page"

Type: string (read-only)

The PartyKit host the client is currently connected to. Useful for debugging custom host configurations.


Signature:

listSharedElements(): Array<{
  type: "source" | "consumer";
  elementId: string;
  dataSource: string;
  normalized: string;
  permissions?: "read-only" | "read-write";
  element: HTMLElement;
}>

Returns a list of every shared-element source and consumer currently registered in the page. Useful for debugging shared / data-source wiring.

See Shared elements for the full guide.


Type: ReadOnlyStore<PlayStore["play"]> (read-only)

A read-only view into the underlying synced data store, keyed by capability tag then element id. Useful for inspecting the raw shared state of all elements in the devtools console. Do not write to this object — mutations will not be validated or synced correctly. Use setData from an element handler instead.


Type: Map<string, Map<string, ElementHandler>>

A nested map of all active element handlers, keyed first by capability tag, then by element id. Useful in devtools for inspecting which elements are registered and accessing their current data.


Type: Map<string, Array<RegisteredPlayEvent>>

A map of all registered event listeners, keyed by event type. Useful for verifying that event listeners were registered correctly and checking for duplicates.