Capabilities
Every capability in playhtml is a single HTML attribute. Slap it on an element, give the element an id, and the library handles sync, persistence, and event wiring. This page catalogs the eight built-ins plus can-play, in order from most-commonly used to most-specialized.
Every section below has the same shape:
- A short description of what the capability does and the kind of state it owns.
- A live demo you can interact with. Open the page in a second tab and watch both update.
- The vanilla HTML form, and the React form. Pick the framework you’re shipping in; the reader’s choice syncs across every snippet on the page.
can-move
Section titled “can-move”What it does. Drag anywhere. Position persists and syncs.
Use it for: drag-and-drop affordances such as fridge magnets, game pieces, arrangeable stickers, a puzzle.
<div id="arena" style="position: relative; height: 320px;">
<img can-move can-move-bounds="arena" id="hat" src="/yankees-hat.png" alt="" />
<img can-move can-move-bounds="arena" id="cat" src="/long-cat.png" alt="" />
</div> import { CanMoveElement } from "@playhtml/react";
<div id="arena" style={{ position: "relative", height: 320 }}>
<CanMoveElement bounds="arena">
<img id="hat" src="/yankees-hat.png" alt="" />
</CanMoveElement>
<CanMoveElement bounds="arena">
<img id="cat" src="/long-cat.png" alt="" />
</CanMoveElement>
</div> Constraining the drag area
Section titled “Constraining the drag area”Use can-move-bounds (vanilla) or the bounds prop (React) to keep the element inside a container. The value is an element id (with or without the leading #) or any valid CSS selector.
<div id="fridge" style="position: relative; height: 400px;">
<div can-move can-move-bounds="fridge" id="magnet-a">🍎</div>
<div can-move can-move-bounds="#fridge" id="magnet-b">🥐</div>
</div> import { CanMoveElement } from "@playhtml/react";
<div id="fridge" style={{ position: "relative", height: 400 }}>
<CanMoveElement bounds="fridge">
<div id="magnet-a">🍎</div>
</CanMoveElement>
<CanMoveElement bounds="#fridge">
<div id="magnet-b">🥐</div>
</CanMoveElement>
</div> The cursor can go past the container edge; only the element’s position is clamped. By default, the keep-visible slice is max(25% of element size, 60px) on every edge, so readers always have something to grab. Two knobs let you tune that:
can-move-bounds-min-visible/boundsMinVisible: fraction(0–1)of the element to keep inside.1pins fully inside (strict clamp);0drops the fraction constraint entirely.can-move-bounds-min-visible-px/boundsMinVisiblePx: absolute pixel floor. Useful when an image has transparent padding around its paint; a pure fraction of the layout bbox can let the visible pixels clip into invisible border.
The effective slice on each axis is max(fraction × size, pxFloor). Set both to 0 to opt fully out of the keep-visible guarantee and let the element slip entirely out of view.
<!-- Keep 50% of the magnet inside the fridge, ignoring the px floor -->
<div can-move can-move-bounds="fridge"
can-move-bounds-min-visible="0.5"
can-move-bounds-min-visible-px="0"
id="magnet">
🧲
</div>
<!-- Small magnet: fraction of 0.25 × 40px = 10px is too small to grab.
The 60px default floor keeps the whole magnet visible regardless. -->
<div can-move can-move-bounds="fridge" id="tiny-magnet" style="width: 40px;">
🌶️
</div> import { CanMoveElement } from "@playhtml/react";
{/* Keep 50% of the magnet inside the fridge, ignoring the px floor */}
<CanMoveElement bounds="fridge" boundsMinVisible={0.5} boundsMinVisiblePx={0}>
<div id="magnet">🧲</div>
</CanMoveElement>
{/* Small magnet: the 60px default floor keeps the whole magnet visible. */}
<CanMoveElement bounds="fridge">
<div id="tiny-magnet" style={{ width: 40 }}>🌶️</div>
</CanMoveElement> can-toggle
Section titled “can-toggle”What it does. Click to flip an on/off boolean. Persists and syncs.
Use it for: shared switches, lamps, “is-this-thing-open” signs, per-element read/unread state. The playhtml homepage uses this on the wordmark letters and the hanging lamp; click either to flip it for everyone.
<button id="my-switch" class="switch" can-toggle>off</button>
<style>
.switch.toggled { background: #6cd97e; }
.switch.toggled::after { content: "on"; }
</style>can-toggle adds the toggled class when the element is on. The older clicked class is still applied for existing styles.
import { CanToggleElement } from "@playhtml/react";
<CanToggleElement>
{({ data }) => {
const on = typeof data === "object" ? data.on : !!data;
return (
<button
id="my-switch"
type="button"
className={on ? "is-on" : "is-off"}
aria-pressed={on}
>
{on ? "on" : "off"}
</button>
);
}}
</CanToggleElement>CanToggleElement already wires up the click handler. Don’t add your own onClick, or you’ll toggle twice per click. Render declaratively from data.
can-grow
Section titled “can-grow”What it does. Click to scale up. Alt-click (or equivalent modifier) to scale down. Persists and syncs.
Use it for: zoomable images, inflating a balloon or sticker, growing a banner over time.
<img can-grow id="balloon" src="/water-balloon.png" alt="" /> import { CanPlayElement } from "@playhtml/react";
import { TagType } from "playhtml";
<CanPlayElement tagInfo={[TagType.CanGrow]} id="balloon" defaultData={{ scale: 1 }}>
{() => <img src="/water-balloon.png" alt="" />}
</CanPlayElement> can-spin
Section titled “can-spin”What it does. Drag to rotate. Persists and syncs.
Use it for: wheels, dials, gauges, spinnable stickers or album covers, “what’s your answer” wheels.
<img can-spin id="wheel" src="/bike-wheel.webp" alt="" /> import { CanPlayElement } from "@playhtml/react";
import { TagType } from "playhtml";
<CanPlayElement tagInfo={[TagType.CanSpin]} id="wheel" defaultData={{ rotation: 0 }}>
{() => <img src="/bike-wheel.webp" alt="" />}
</CanPlayElement> can-hover
Section titled “can-hover”What it does. Shares hover state across everyone on the page. Presence-based, not persistent: when a user leaves, their hover clears.
Use it for: “who’s looking at this right now” affordances, social read-receipts, zero-latency shared hover effects.
How it works. While anyone (you or another reader) is hovering the element, playhtml sets a data-playhtml-hover attribute on it; when nobody is hovering, the attribute is removed. Style the effect by targeting [data-playhtml-hover] instead of the :hover pseudo-class. That’s the only change from a normal hover style, and it’s the same attribute whether you use the vanilla capability or the React component.
<div can-hover id="hover-pad">hover me</div>
<style>
/* Use [data-playhtml-hover], NOT :hover, so the effect fires
for everyone when anyone on the page hovers the element. */
#hover-pad[data-playhtml-hover] {
background: #fde047;
transform: scale(1.05);
}
</style> import { CanHoverElement } from "@playhtml/react";
<CanHoverElement>
<div id="hover-pad">hover me</div>
</CanHoverElement>/* Same attribute as the vanilla capability */
#hover-pad[data-playhtml-hover] {
background: #fde047;
transform: scale(1.05);
} If you want the hover effect to reflect who is hovering (e.g. tint the element with each viewer’s cursor color) rather than a plain on/off, read the hover roster off element awareness. Awareness is presence scoped to one element, so it clears when readers leave and does not persist:
import { CanPlayElement } from "@playhtml/react";
import { TagType } from "playhtml";
<CanPlayElement
tagInfo={[TagType.CanHover]}
id="hover-pad"
defaultData={{}}
myDefaultAwareness={"#3b82f6"}
>
{({ awareness }) => (
<div
style={{
background: `linear-gradient(45deg, ${awareness.join(", ")})`,
}}
>
hover me
</div>
)}
</CanPlayElement>
can-duplicate
Section titled “can-duplicate”What it does. Click a trigger to clone an existing element into a new one. Every clone is independent shared state.
Use it for: user-generated galleries, seed-from-template UIs, stamps, “leave your mark” patterns.
<img id="bunny-template" src="/pixel-bunny.png" alt="" />
<button can-duplicate="bunny-template" id="clone-btn">clone a bunny</button>
<script>
// The "reset" button sweeps every spawned rabbit from shared state.
// Clones get an id of the form "bunny-template-<random>", so match on that prefix.
document.getElementById("reset-btn").addEventListener("click", () => {
document.querySelectorAll("[id^='bunny-template-']").forEach((el) => {
playhtml.deleteElementData("can-duplicate", el.id);
el.remove();
});
});
</script>
<button id="reset-btn">reset</button>By default, clones are inserted right after the template (or the previous clone). Add can-duplicate-to (an element id or CSS selector) to drop every clone into a specific container instead:
<button
can-duplicate="bunny-template"
can-duplicate-to="#bunny-pen"
id="clone-btn"
>clone a bunny</button>
<div id="bunny-pen"></div> import { useRef } from "react";
import { CanDuplicateElement } from "@playhtml/react";
// CanDuplicateElement takes refs, not selectors. `elementToDuplicate` is the
// template that gets cloned; `canDuplicateTo` is the React equivalent of the
// can-duplicate-to attribute. Both elements need a stable `id`.
function BunnyField() {
const template = useRef<HTMLDivElement>(null);
const field = useRef<HTMLDivElement>(null);
return (
<div>
<div id="bunny-template" ref={template}>🐰</div>
<div id="bunny-field" ref={field} />
<CanDuplicateElement elementToDuplicate={template} canDuplicateTo={field}>
<button>clone a bunny</button>
</CanDuplicateElement>
</div>
);
} can-mirror
Section titled “can-mirror”can-mirror syncs every change to the element — attributes, child nodes, and form state — to everyone, and saves them. You don’t write any data or callbacks; just change the element with normal HTML, CSS, and JavaScript. Use it when the state you want to share is the DOM itself: a shared textarea, a form, a growing list of children.
Full treatment with live demos lives on Custom elements → can-mirror, and the mirror playground walks every edge case (hover, focus, every form input, contenteditable, programmatic attribute changes).
can-play
Section titled “can-play”Build your own capability: you define a shared data shape and how the element renders from it (an imperative updateElement, or the newer reactive view). You control how the data drives the element and which parts persist. Use it for custom counters, guestbooks, chat, games, reactions, per-user state, and event broadcasts — anything the built-ins don’t cover.
Full guide, with the imperative and reactive view forms (vanilla) and withSharedState (React) side by side — plus live demos: Custom elements.
Composed examples
Section titled “Composed examples”These capabilities are the building blocks. The best way to see them in concert is to visit the live experiments room, where readers leave permanent marks in a shared space.
These three are highlighted in the homepage experiments index, the fastest way to see playhtml at room-scale.