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.
This page has live demos and both framework forms. For attribute names, data shapes, and options, see the capabilities reference.
Two things to know
Every element needs a stable id
The id is the key an element’s synced data is stored under, so keep it the same across reloads and browsers. A hand-written id="my-lamp" is ideal. For a row of identical elements, use selector-id instead. See Core concepts.
Shift-click resets data
Shift-clicking a can-move, can-spin, can-toggle, or can-grow element resets it to its default state for everyone in the room (back to its origin, 0°, off, or scale 1). Custom can-play elements can opt in with the resetShortcut option.
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 constrain an element while it is dragged.
The value is an element id (with or without the leading #) or any valid CSS selector.
Bounds apply only during dragging. They do not rewrite the element’s initial CSS layout or persisted position during setup.
<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. During a drag, only the element’s position is clamped. By default, the whole element stays inside the container. Two knobs let you opt into partial overhang while keeping a visible slice inside:
can-move-bounds-min-visible/boundsMinVisible: fraction(0–1)of the element to keep inside. Default1pins fully inside; lower values allow part of the element to hang over the edge.0drops the fraction constraint entirely.can-move-bounds-min-visible-px/boundsMinVisiblePx: absolute pixel floor. Default60; it applies whenmin-visibleallows partial overhang.
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>
<!-- Default bounds keep the whole magnet visible while dragging. -->
<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>
<span class="off-label">off</span>
<span class="on-label">on</span>
</button>
<style>
/* can-toggle adds the "toggled" class while the switch is on */
.switch.toggled { background: #274b9e; color: white; }
.on-label,
.switch.toggled .off-label { display: none; }
.switch.toggled .on-label { display: inline; }
</style> import { CanToggleElement } from "@playhtml/react";
<CanToggleElement>
{({ data }) => (
<button id="my-switch" className="switch">
{data.on ? "on" : "off"}
</button>
)}
</CanToggleElement>CanToggleElement adds the same toggled class and already handles the click. Don’t add your own onClick, or the switch flips twice per click.
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 { CanGrowElement } from "@playhtml/react";
<CanGrowElement>
<img id="balloon" src="/water-balloon.png" alt="" />
</CanGrowElement> 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 { CanSpinElement } from "@playhtml/react";
<CanSpinElement>
<img id="wheel" src="/bike-wheel.webp" alt="" />
</CanSpinElement> 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 (for example, tint the element with each hovering viewer’s color) rather than a plain on/off, pass a function child and read everyone. Each person’s presence.hover says whether they’re hovering right now, and color is their color. It clears when they leave and does not persist:
import { CanHoverElement } from "@playhtml/react";
<CanHoverElement>
{({ everyone }) => {
const colors = everyone
.filter((person) => person.presence.hover)
.map((person) => person.color);
return (
<div
id="hover-pad"
style={{
background:
colors.length > 1
? `linear-gradient(45deg, ${colors.join(", ")})`
: colors[0],
}}
>
hover me
</div>
);
}}
</CanHoverElement>
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.
<div id="bunny-meadow">
<img id="bunny-template" src="/pixel-bunny.png" alt="" />
</div>
<button id="clone-btn" can-duplicate="bunny-template" can-duplicate-to="bunny-meadow">
clone a bunny
</button>
<button id="clear-btn">clear</button>
<script type="module">
import { playhtml } from "https://unpkg.com/playhtml@latest";
playhtml.init();
// The clone button stores its clones' ids, and each clone's id starts with
// "bunny-template-". Clear both to empty the meadow for everyone.
document.getElementById("clear-btn").addEventListener("click", () => {
document.querySelectorAll("[id^='bunny-template-']").forEach((el) => el.remove());
playhtml.deleteElementData("can-duplicate", "clone-btn");
});
</script>Clones go into the can-duplicate-to container (an element id or CSS selector). Without it, each clone is inserted right after the template, or after the previous clone.
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 BunnyMeadow() {
const template = useRef<HTMLImageElement>(null);
const meadow = useRef<HTMLDivElement>(null);
return (
<>
<div id="bunny-meadow" ref={meadow}>
<img id="bunny-template" ref={template} src="/pixel-bunny.png" alt="" />
</div>
<CanDuplicateElement elementToDuplicate={template} canDuplicateTo={meadow}>
<button id="clone-btn">clone a bunny</button>
</CanDuplicateElement>
</>
);
} can-mirror
Section titled “can-mirror”can-mirror syncs changes on the element you put it on: its attributes, its direct child list, and form or contenteditable state that surfaces through input/change events. It does not watch arbitrary descendant DOM mutations. If you want to sync nested children changes, put can-mirror on that nested element too and give it a stable id.
Full treatment with live demos lives on Custom elements → can-mirror, and the mirror playground shows examples of all the common native HTML element interactions with it.
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 update, 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 live demos: Custom elements. Property reference: Element API.
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.