Getting started
playhtml turns any HTML element into a live, collaborative one with a single attribute. This guide gets you from empty page to a working shared toggle.
Pick your setup:
There are two paths. Pick the one that matches your project.
Path A: drop-in script (no build, no module syntax)
Section titled “Path A: drop-in script (no build, no module syntax)”The fastest path, with no import and no playhtml.init() call. Add the script tag at the end of your <body>, after your interactive elements:
<body>
<button id="my-lamp" can-toggle>lamp</button>
<script type="module" src="https://unpkg.com/playhtml/dist/init.es.js"></script>
</body>Give every interactive element a unique id. That’s how state is keyed and synced across everyone on the page.
Why end-of-body? The drop-in script runs
playhtml.init()immediately, andinit()scans the DOM for capability attributes (can-toggle,can-move, etc.) when it runs. Place it after the elements it should find.
Want options like cursors? Use Path B. The drop-in script calls
playhtml.init({})with no options.
Path B: import + manual init (when you need options or a bundler)
Section titled “Path B: import + manual init (when you need options or a bundler)”Reach for this when you want to pass options to init() (cursors, custom rooms, etc.) or you’re already using a bundler.
From a CDN, no build step: put the script at the end of <body> so the elements exist when init() runs.
<body>
<button id="my-lamp" can-toggle>lamp</button>
<script type="module">
import { playhtml } from "https://unpkg.com/playhtml";
playhtml.init({ cursors: { enabled: true } });
</script>
</body>From npm with a bundler:
npm install playhtmlimport { playhtml } from "playhtml";
playhtml.init({ cursors: { enabled: true } });Bundlers usually handle script placement for you; just make sure init() runs after your interactive elements are in the DOM (e.g. on DOMContentLoaded or after your framework mounts).
Either way, give every interactive element a unique id. That’s how state is keyed and synced across everyone on the page.
Install
Section titled “Install”npm install playhtml @playhtml/reactInitialize
Section titled “Initialize”Wrap your tree in PlayProvider once, then drop in a capability component. Cursors are opt-in.
import { PlayProvider, CanToggleElement } from "@playhtml/react";
export function App() {
return (
<PlayProvider initOptions={{ cursors: { enabled: true } }}>
<CanToggleElement>
<button id="my-lamp">lamp</button>
</CanToggleElement>
</PlayProvider>
);
}Need more control? Use CanPlayElement with a render prop to read and write the shared data directly:
import { CanPlayElement } from "@playhtml/react";
import { TagType } from "playhtml";
<CanPlayElement
tagInfo={[TagType.CanToggle]}
id="my-lamp"
defaultData={{ on: false }}
>
{({ data, setData }) => (
<button onClick={() => setData({ on: !data.on })}>
{data.on ? "on" : "off"}
</button>
)}
</CanPlayElement> Try it live
Section titled “Try it live”This toggle is shared with everyone reading this page right now. Click it to see it update for all readers.
Where to next
Section titled “Where to next”- Core concepts: the four kinds of shared state (element data, page data, presence, events) and when to use each.
- Using React: if your app is a React app, start here; concept pages show React inline.
- Capabilities: every built-in
can-*attribute with live demos. - Data essentials: shape, update, and clean up
defaultData. - Presence & cursors: multiplayer cursors and ephemeral per-user state.
- Shared elements: cross-page and cross-domain state.
- Building with AI: Claude Code plugin + a prompt template for any LLM.
- API reference: the full
playhtml.init()options table and React API types.