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 — no import, 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/init.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?
init.jsrunsplayhtml.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
init.jscallsplayhtml.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/react @playhtml/commonInitialize
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/common";
<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 and watch.
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.