Element initializer API
The ElementInitializer configures a custom collaborative element. Use the same shape in three supported APIs:
playhtml.register(elementOrId, initializer)for one elementplayhtml.define(name, initializer)for a reusable capabilityextraCapabilitiesinplayhtml.init()for capabilities declared during initialization
The initializer has three state buckets: shared data, per-user localData, and ephemeral awareness.
For usage examples, see Custom elements.
Directly assigning initializer fields to an element remains supported for compatibility, but is deprecated for vanilla code. See Direct element properties.
Full shape
Section titled “Full shape”Callback context (ctx)
Section titled “Callback context (ctx)”Passed to updateElement, view, onClick, onDrag, and onDragStart:
{
data, // shared synced state (read-only snapshot)
localData, // per-user, per-tab; not synced
awareness, // array of every user's awareness value for this element
awarenessByStableId, // Map<stableId, awareness value>
element, // the HTMLElement
setData, // (next) mutator fn or replacement object
setLocalData, // (next) mutator fn or replacement; re-renders view
setMyAwareness, // (next) your ephemeral awareness value
requestUpdate, // () re-run view now; no-op without view
}
updateElementAwareness receives the same fields, plus myAwareness (your own awareness value).
Do not call setData, setLocalData, or setMyAwareness during a view render — playhtml logs an error and ignores the write. setData merge rules: Data essentials.
onMount context
Section titled “onMount context”onMount uses getters instead of live values (the callback outlives individual renders):
{
getData, // () => current shared data
getLocalData, // () => current local data
getAwareness, // () => awareness array
getElement, // () => the HTMLElement
setData, // same setters as ctx
setLocalData,
setMyAwareness,
requestUpdate,
}
Initializer
Section titled “Initializer”Everything you can put in an initializer for register, define, or extraCapabilities:
{
// Starting shared state for elements that render from shared data. Must be
// an object (or a function that returns one) — not a bare number or string.
// Synced across the room. Must be paired with updateElement or view.
defaultData: { count: 0 },
// defaultData: (element) => ({ color: element.dataset.color }),
// Per-user, per-tab state. Never synced. Drag anchors, drafts, UI flags.
defaultLocalData: undefined,
// Your starting awareness value for this element. Ephemeral — clears on
// disconnect. Pair with updateElementAwareness.
myDefaultAwareness: undefined,
// --- data update path: provide updateElement OR view, not both ---
updateElement(ctx) {
// ctx — see Callback context above. Write the DOM from ctx.data.
// Do not call ctx.setData here — it loops.
},
view(ctx) {
// ctx — see Callback context above. Return a lit-html template.
// Mutually exclusive with updateElement, onClick, onDrag, onDragStart.
// Drive ctx.setData from @click handlers, not during render.
},
updateElementAwareness(ctx) {
// ctx — Callback context + myAwareness. Pair with myDefaultAwareness.
},
// --- event handlers (ignored when using view) ---
onClick(e, ctx) {}, // e: MouseEvent; ctx — Callback context
onDragStart(e, ctx) {}, // e: MouseEvent | TouchEvent, once at drag start
onDrag(e, ctx) {}, // e: MouseEvent | TouchEvent, each move until release
onMount(ctx) {
// ctx — see onMount context above (getters, not live values).
// May fire before the room's first sync. For presence or page data:
// playhtml.ready.then(() => { ... });
return () => {}; // cleanup on unmount
},
// Modifier + click resets to defaultData for everyone. Built-ins use "shiftKey".
resetShortcut: "shiftKey", // "ctrlKey" | "altKey" | "shiftKey" | "metaKey"
debounceMs: undefined, // optional debounce on sync writes; rarely needed
// For define / extraCapabilities: return false to skip this element.
isValidElementForTag(element) { return true; },
}
defaultData
Section titled “defaultData”Required. Starting shared state for new elements. Must be an object (or a function that returns one), not a bare primitive like 0 or "".
const initializer = {
defaultData: { count: 0 },
};
// Or derive from the element:
const derivedInitializer = {
defaultData: (element) => ({ color: element.dataset.color ?? "yellow" }),
};
defaultLocalData
Section titled “defaultLocalData”Per-user, per-tab state that is not synced. Use for drag anchors, hover flags, or UI that only the local client needs.
const initializer = {
defaultLocalData: { draft: "" },
};
myDefaultAwareness
Section titled “myDefaultAwareness”Your starting awareness value for this element. Awareness is ephemeral — it clears when you disconnect. Other clients read it through the awareness array in callbacks.
const initializer = {
myDefaultAwareness: "#2563eb",
};
updateElement
Section titled “updateElement”Imperative update path. playhtml calls it on mount and whenever shared data, localData, or awareness changes (locally or from another tab). Write the DOM from ctx.data.
Mutually exclusive with view. Do not call setData inside updateElement — that creates a write loop. See Data essentials rule 7.
const initializer = {
updateElement: ({ element, data }) => {
element.textContent = String(data.count);
},
};
Experimental. Declarative update path. Return a lit-html template; playhtml patches the DOM when state changes.
Mutually exclusive with updateElement, onClick, onDrag, and onDragStart — put events in the template (@click, etc.).
See Registration API for register, define, helpers, and handle methods.
updateElementAwareness
Section titled “updateElementAwareness”Called when element awareness changes. Same context as updateElement, plus myAwareness (your own value).
const initializer = {
updateElementAwareness: ({ element, awareness }) => {
element.dataset.viewers = String(awareness.length);
},
};
If the element also has a view, awareness changes re-render the view automatically.
onClick
Section titled “onClick”Fired on click. Ignored when the element uses view.
const initializer = {
onClick: (_event, { setData }) => {
setData((data) => {
data.count += 1;
});
},
};
onDrag and onDragStart
Section titled “onDrag and onDragStart”Drag handlers for mouse and touch. onDragStart runs once when the drag begins; onDrag runs on each move until release. Ignored when the element uses view.
Built-in capabilities like can-move and can-spin use these internally.
onMount
Section titled “onMount”Runs once when the element is wired up. Use for listeners, timers, or requestAnimationFrame loops tied to this element.
Return a cleanup function when the element is removed.
onMount vs playhtml.ready: onMount fires when this element’s handler attaches — that can happen before the room’s first sync finishes. data may still be defaultData until sync lands. Use onMount alone for element-scoped setup (listeners, animation loops). Wait on playhtml.ready inside onMount when you need room-wide state that only exists after sync (presence, createPageData, reading final server data):
const initializer = {
onMount: ({ getData, setData }) => {
let cancelled = false;
playhtml.ready.then(() => {
if (cancelled) return;
const presences = playhtml.presence.getPresences();
setData((data) => {
data.viewerCount = presences.size;
});
});
return () => {
cancelled = true;
};
},
};
For a clock-driven view, requestAnimationFrame in onMount is enough — you do not need playhtml.ready:
const initializer = {
onMount: ({ getData, requestUpdate }) => {
let raf;
const tick = () => {
if (getData().running) requestUpdate();
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
},
};
resetShortcut
Section titled “resetShortcut”When set, a click with that modifier held resets the element to defaultData for everyone in the room.
Built-in capabilities use "shiftKey". Custom elements opt in the same way:
const initializer = {
resetShortcut: "shiftKey",
};
debounceMs
Section titled “debounceMs”Optional debounce window for the handler’s internal sync callback. Most custom elements call setData from event handlers, which syncs immediately. You rarely need this.
isValidElementForTag
Section titled “isValidElementForTag”Gate which DOM elements a reusable capability (define / extraCapabilities) applies to. Return false to skip an element that carries the attribute but should not bind (for example, when a referenced template is missing).
Registering an initializer
Section titled “Registering an initializer”Pass the initializer to playhtml.register. The element needs a stable, unique id; the can-play attribute is optional because register supplies the custom capability.
<div id="counter"></div>
<script type="module">
import { playhtml } from "playhtml";
playhtml.register("counter", {
defaultData: { count: 0 },
onClick: (_event, { setData }) => {
setData((data) => {
data.count += 1;
});
},
updateElement: ({ element, data }) => {
element.textContent = String(data.count);
},
});
playhtml.init();
</script>
register can run before or after playhtml.init(), and before or after the element exists. It binds when both the registration and element are available.
When the same element also has a built-in capability such as can-move, each capability keeps its own data and behavior.
Direct element properties (deprecated)
Section titled “Direct element properties (deprecated)”Direct assignments such as element.defaultData = … and element.updateElement = … remain supported so existing sites and React integrations keep working. Do not use them in new vanilla code. Move every initializer field into the object passed to register.
The compatibility path reads these keys from the element: defaultData, defaultLocalData, myDefaultAwareness, updateElement, view, updateElementAwareness, onClick, onDrag, onDragStart, onMount, resetShortcut, debounceMs, and isValidElementForTag.
If existing code assigns those properties and calls setupPlayElement(element), replace both steps with register(element, initializer). setupPlayElement remains supported for dynamically added built-in and defined capabilities.
Validation
Section titled “Validation”At registration time, playhtml checks:
defaultDataandupdateElement/vieware provided togethermyDefaultAwareness, when present, is paired withupdateElementAwareness- At least one update function exists:
updateElement,view, orupdateElementAwareness register/definethrow if bothviewandupdateElementare set, or ifviewis combined withonClick/onDrag/onDragStart
If validation fails, the element is skipped and a console error lists the missing or invalid pair.