playhtml client
The playhtml object is the library entry point. Import it from the package or from a CDN.
npm with a bundler:
import { playhtml } from "playhtml";
await playhtml.init();
CDN without a bundler:
<script type="module">
import { playhtml } from "https://unpkg.com/playhtml";
await playhtml.init();
</script>
API groups at a glance:
| Group | Members |
|---|---|
| Lifecycle | init, configure, ready, isLoading, handleNavigation |
| Element setup & teardown | setupPlayElements, setupPlayElement, setupPlayElementForTag, removePlayElement, deleteElementData |
| Custom elements | register, define, getHandle |
| Events | dispatchPlayEvent, registerPlayEventListener, removePlayEventListener |
| Page data | createPageData |
| People & presence | people, createPresenceRoom, cursorClient, users and presence (deprecated) |
| Inspection | roomId, host, syncedStore, listSharedElements |
Lifecycle
Section titled “Lifecycle”init(options?)
Section titled “init(options?)”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.
configure(options?)
Section titled “configure(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 room has synced and elements are active. Rejects if the connection fails.
When client-side navigation moves to a different room (see handleNavigation()), ready is replaced by a new promise that resolves once the new room has synced. Read playhtml.ready again after navigating rather than holding on to an old one.
playhtml.init();
// Await sync before reading shared state
await playhtml.ready;
console.log("synced:", playhtml.roomId);
isLoading
Section titled “isLoading”Type: boolean (read-only)
true from the time init() is called until the room has synced and elements are active. It turns true again while client-side navigation loads a different room, and back to false once that room is in. Useful for showing a loading indicator.
Each time it flips, playhtml dispatches a playhtml:loading-change event on document with detail.isLoading.
if (playhtml.isLoading) {
document.body.classList.add("loading");
}
document.addEventListener("playhtml:loading-change", (event) => {
document.body.classList.toggle("loading", event.detail.isLoading);
});
handleNavigation()
Section titled “handleNavigation()”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.
Element setup & teardown
Section titled “Element setup & teardown”setupPlayElement(element, options?)
Section titled “setupPlayElement(element, options?)”Signature: setupPlayElement(element: Element, options?: { ignoreIfAlreadySetup?: boolean }): void
Registers one element after init(). Use for dynamically added built-in or defined capabilities.
Pass { ignoreIfAlreadySetup: true } to skip elements that are already registered.
The element needs a unique id.
For a one-off custom element, use register(elementOrId, initializer). It binds the element automatically, so it does not need a separate setupPlayElement call.
const card = document.createElement("div");
card.id = "card-1";
card.setAttribute("can-move", "");
document.body.appendChild(card);
playhtml.setupPlayElement(card);
setupPlayElements()
Section titled “setupPlayElements()”Signature: setupPlayElements(): void
Scans the entire document and registers elements that carry a playhtml capability attribute. Elements already bound to the same DOM node are skipped. Called internally at the end of init(). Use it after injecting a batch of playhtml elements into the DOM.
container.innerHTML = serverRenderedHtml;
playhtml.setupPlayElements();
setupPlayElementForTag(element, tag)
Section titled “setupPlayElementForTag(element, tag)”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");
removePlayElement(element)
Section titled “removePlayElement(element)”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"));
deleteElementData(tag, elementId)
Section titled “deleteElementData(tag, elementId)”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.
Custom elements
Section titled “Custom elements”Use register for one custom element and define for a reusable capability. Both accept an ElementInitializer with either the supported imperative update renderer or the experimental declarative view renderer. See Registration API.
register(elementOrId, init)
Section titled “register(elementOrId, init)”Signatures:
register<T, U, V>(
elementId: string,
init: ElementInitializer<T, U, V>,
): PlayElementHandle<T, U, V>
register<T, U, V>(
element: HTMLElement,
init: ElementInitializer<T, U, V>,
): PlayElementHandle<T, U, V>
Binds an initializer to one element. Pass a string id before the element exists, or pass an existing HTML element to bind that node directly. The element form requires a non-empty id. Both forms return a handle for reads and writes outside the element’s callbacks.
const counter = document.getElementById("my-counter");
const handle = playhtml.register(counter, {
defaultData: { count: 0 },
onClick: (_event, { setData }) => {
setData((data) => {
data.count += 1;
});
},
update: ({ element, data }) => {
element.textContent = `Clicked ${data.count} times`;
},
});
Directly assigning initializer fields to an element remains supported for compatibility, but is deprecated for vanilla code.
define(capabilityName, init)
Section titled “define(capabilityName, init)”Signature: define<T, U, V>(capabilityName: string, init: ElementInitializer<T, U, V>): void
Registers a reusable capability under an attribute name. Matching elements already on the page bind immediately, as do matching descendants rendered by a registered view. Call setupPlayElement(element) for other elements added after initialization. define is 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.
getHandle(elementId, capability?)
Section titled “getHandle(elementId, capability?)”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
Section titled “Events”Events let you broadcast custom actions across all connected clients in the room. See Events for the full guide.
registerPlayEventListener(type, event)
Section titled “registerPlayEventListener(type, event)”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: T) => void; // the payload passed to dispatchPlayEvent
}
const listenerId = playhtml.registerPlayEventListener("confetti", {
onEvent: () => window.confetti({ particleCount: 100 }),
});
Multiple listeners for the same type are allowed — each gets its own id.
dispatchPlayEvent(message)
Section titled “dispatchPlayEvent(message)”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" } });
removePlayEventListener(type, id)
Section titled “removePlayEventListener(type, id)”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
Section titled “Page data”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.
createPageData(name, defaultValue)
Section titled “createPageData(name, defaultValue)”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:
| Method | Description |
|---|---|
getData(): T | Returns the current value. |
setData(data: T | updater): void | Updates the value. Mutate object/array drafts in place; an updater for a primitive returns its next value. |
onUpdate(callback: (data: T) => void): () => void | Subscribes to changes. Synchronous writes produce one callback with the final value. Returns an unsubscribe function. |
destroy(): void | Tears down the channel and removes all subscriptions. |
Throws if called before init() completes sync.
People & presence
Section titled “People & presence”people is who’s on the page and what each of them is doing right now. See People & presence for types and API details. Usage guide: People and presence.
people
Section titled “people”Type: People
Everyone on the page, including you, as Person objects (pid, name, color, isMe, presence), whether or not cursors are enabled. This is the place to read or set your name and color, and to set your page presence. Throws if accessed before init() completes.
await playhtml.ready;
playhtml.people.me.name = "Alice";
playhtml.people.setMyPresence("status", "typing");
playhtml.people.everyone; // [{ pid, name, color, isMe, presence }, ...]
const unsubscribe = playhtml.people.onChange("status", (everyone) => {
renderStatusRow(everyone);
});
| Member | Description |
|---|---|
everyone | Everyone here (including you), in join order. getAll() returns the same list. |
me | You. Assign me.name or me.color to change them for everyone. me.presence is your page presence. |
setMyPresence(key, value) | Sets one key of your presence. null removes it. |
onChange(keys?, callback) | Calls back right away and on joins, leaves, name or color changes, and presence changes (only the given keys, if any). Cursor moves only with "cursor". Returns an unsubscribe function. |
createPresenceRoom(name)
Section titled “createPresenceRoom(name)”Signature: createPresenceRoom(name: string): PresenceRoom
Creates a separate presence scope (sidebar, lobby, shared document). room.people has the same API as people, without cursors. Call room.destroy() when done.
await playhtml.ready;
const sidebar = playhtml.createPresenceRoom("sidebar");
sidebar.people.setMyPresence("focus", { section: "comments" });
// Tear down when done
sidebar.destroy();
Throws if called before init() completes sync.
users (deprecated)
Section titled “users (deprecated)”Type: { me, getAll(), onChange(cb) }
The earlier identity API. It keeps working: users.me is the same as people.me without presence, and users.getAll() returns everyone without presence. Use people.
presence (deprecated)
Section titled “presence (deprecated)”Type: PresenceAPI
The earlier channel-based presence object. It keeps working, but use people. See the migration table.
cursorClient
Section titled “cursorClient”Type: CursorClientAwareness | null
Low-level cursor client, or null if cursors are disabled. Most config belongs in init({ cursors: … }), and cursor positions are easier to read from each person’s presence.cursor in people. See Cursors.
Inspection
Section titled “Inspection”Read-only debug surfaces. Do not mutate them directly.
roomId
Section titled “roomId”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.
listSharedElements()
Section titled “listSharedElements()”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.
syncedStore
Section titled “syncedStore”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.