| name | building-playhtml-elements |
| description | Use when building, creating, or implementing a playhtml element or component, whether in vanilla HTML or React. Triggers include requests to make interactive, collaborative, real-time, or multiplayer HTML elements. |
Building playhtml Elements
playhtml makes HTML elements collaborative and real-time via Yjs CRDTs.
Before Implementing — Ask These Questions
If the user's request is ambiguous on ANY of these, stop and ask:
- Persistence: Should data survive page refresh? (defaultData=yes, awareness=no)
- Shared vs per-user: Should all users see the same state, or does each user have their own?
- Vanilla HTML or React?
These determine which API and data type to use. Getting them wrong means a rewrite.
Data Types
| Type | Persists? | Syncs? | Use for |
|---|
defaultData | Yes | Yes | Positions, counts, messages, toggles |
myDefaultAwareness | No | Yes | Who's online, typing, hover state |
dispatchPlayEvent | No | One-shot | Confetti, notifications |
localStorage | Yes | No | Per-user flags ("has reacted") |
Critical Rules
- Every element MUST have a unique
id attribute — without it, sync silently fails
- Vanilla HTML: Configure element properties BEFORE
playhtml.init() (the #1 mistake)
- React: Wrap app in
<PlayProvider>
Quick Reference — Vanilla HTML (can-play)
const el = document.getElementById("myElement");
el.defaultData = { count: 0 };
el.updateElement = ({ element, data }) => { ... };
el.onClick = (e, { data, setData }) => { ... };
el.onDrag = (e, { data, setData, localData, setLocalData }) => { ... };
el.onDragStart = (e, { setLocalData }) => { ... };
el.onMount = ({ getData, setData, getElement }) => { ... };
el.resetShortcut = "shiftKey";
import { playhtml } from "https://unpkg.com/playhtml@latest";
playhtml.init();
Quick Reference — React (withSharedState)
import { PlayProvider, withSharedState, usePlayContext } from "@playhtml/react";
const Counter = withSharedState(
{ defaultData: { count: 0 } },
({ data, setData, ref }) => (
<button ref={ref} onClick={() => setData({ count: data.count + 1 })}>
{data.count}
</button>
)
);
setData — Two Forms
setData({ ...data, count: data.count + 1 });
setData((draft) => { draft.items.push(newItem); });
Built-in Capabilities
Use instead of can-play when they fit: can-move, can-toggle, can-spin, can-grow, can-duplicate, can-mirror. See packages/common/src/index.ts for implementations.
Cursors (optional)
playhtml.init({ cursors: { enabled: true, room: "page" } });
window.cursors.allColors.length;
See https://playhtml.fun/docs/data/presence/cursors/ for full API.
Common Mistakes
- Config after init (vanilla): Properties set after
playhtml.init() are ignored. Configure FIRST.
- Missing
id: No id = no sync. Silent failure.
- Wrong data type: Awareness for persistent data (disappears on disconnect) or defaultData for ephemeral presence (leaves stale data). Refer to the Data Types table.
- Bad array mutations: In mutator form, the draft is a Yjs CRDT proxy. Use
push()/splice() only — shift(), pop(), and items[i] = x don't sync correctly.
- Value form loses fields:
setData({ x: 5 }) erases y. Always spread: setData({ ...data, x: 5 }) or use mutator form.
- Deep nesting: CRDTs work best with flat data. Avoid deeply nested objects.
- High-frequency updates: Don't
setData on every mousemove. Debounce, or use setLocalData/awareness.
- Computed values in state: Don't store what you can calculate. Compute in
updateElement/render.
- Missing PlayProvider (React):
withSharedState silently fails without it.