From playhtml
Guides building collaborative real-time HTML elements with playhtml and Yjs CRDTs in vanilla HTML or React. Covers data types, persistence, shared state, awareness, and event handlers.
How this skill is triggered — by the user, by Claude, or both
Slash command
/playhtml:building-playhtml-elementsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
playhtml makes HTML elements collaborative and real-time via Yjs CRDTs.
playhtml makes HTML elements collaborative and real-time via Yjs CRDTs.
If the user's request is ambiguous on ANY of these, stop and ask:
These determine which API and data type to use. Getting them wrong means a rewrite.
| 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") |
id attribute — without it, sync silently failsplayhtml.init() (the #1 mistake)<PlayProvider>const el = document.getElementById("myElement");
el.defaultData = { count: 0 }; // REQUIRED
el.updateElement = ({ element, data }) => { ... }; // REQUIRED
el.onClick = (e, { data, setData }) => { ... };
el.onDrag = (e, { data, setData, localData, setLocalData }) => { ... };
el.onDragStart = (e, { setLocalData }) => { ... };
el.onMount = ({ getData, setData, getElement }) => { ... };
el.resetShortcut = "shiftKey"; // "shiftKey"|"ctrlKey"|"altKey"|"metaKey"
// THEN import — ordering matters!
import { playhtml } from "https://unpkg.com/playhtml@latest";
playhtml.init();
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>
)
);
// Component receives: data, setData, awareness, setMyAwareness, ref
// For events: usePlayContext() → { dispatchPlayEvent, registerPlayEventListener }
// For cursors: usePlayContext() → { cursors, configureCursors }
// Value form: replaces ALL data (spread to preserve other fields!)
setData({ ...data, count: data.count + 1 });
// Mutator form: modify in place (preferred for arrays/nested)
setData((draft) => { draft.items.push(newItem); });
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.
playhtml.init({ cursors: { enabled: true, room: "page" } }); // "page"|"domain"|"section"
window.cursors.allColors.length; // user count
See docs/cursors.md for full API.
playhtml.init() are ignored. Configure FIRST.id: No id = no sync. Silent failure.push()/splice() only — shift(), pop(), and items[i] = x don't sync correctly.setData({ x: 5 }) erases y. Always spread: setData({ ...data, x: 5 }) or use mutator form.setData on every mousemove. Debounce, or use setLocalData/awareness.updateElement/render.withSharedState silently fails without it.npx claudepluginhub spencerc99/playhtmlGenerates self-contained HTML playgrounds with controls, live previews, and copyable prompts for interactive exploration of design, data, code review, and architecture topics.
Creates self-contained interactive HTML playgrounds with controls, live previews, and copyable prompts for visual exploration of design, data, code, and documents.
Create HTML playgrounds with sliders, knobs, toggles, and live preview for tuning algorithm parameters, animation values, design tokens, layout dimensions, or any value that's painful to express in text. Always include a Submit button (calls `submitToClaude`) so chosen values can be sent back to Claude Code. Use whenever the user wants to experiment with values, fine-tune behaviors, explore a parameter space, or pick from a continuous range — debounce timings, color values, easing curves, threshold values, layout dimensions, anything tunable.