Skip to content
playhtml

can-mirror playground

can-mirror is the “everything syncs” capability. Put it on any element and playhtml observes the DOM — input values, checked state, pseudo-class data attributes, attribute changes, child adds/removes — then replays those mutations on every other reader.

This page hosts every edge case we’ve found useful, one section each. Open it in two browser windows side by side and poke at each demo — everything should mirror.

Nothing on this page requires custom JavaScript. It’s all plain HTML with can-mirror on a root element. The docs site’s shared playhtml.init() picks each one up automatically.

The synced data-playhtml-hover attribute lets CSS target the hover state from across the network. Hover over the box in one tab — it turns green in every other tab.

Hover me

<style>
  #pg-mirror-hover[data-playhtml-hover] {
    background: #4caf50;
    color: white;
  }
</style>

<div can-mirror id="pg-mirror-hover">Hover me</div>

Click the input — the blue outline shows for every client until you blur.

Type — the value replicates.



Expand and collapse — the native open attribute mirrors automatically.

Click to expand

Hidden until the details element is opened. The open attribute is a regular DOM attribute, so the MutationObserver catches it automatically.

Type, paste, delete — every mutation lands on every client. Try pressing Enter inside the list to add items; delete them by selecting and pressing Backspace.

Edit this text collaboratively!

  • First item — type to edit, Enter for new line
  • Second item

A single can-mirror wrapping a form with heterogeneous inputs — all of them sync together.

can-mirror listens to a MutationObserver on the element. It doesn’t care whether a mutation comes from a click, a React re-render, or your browser devtools. You can verify that last one right now.

Open your browser devtools (F12) and paste this into the console:

const el = document.querySelector("#pg-mirror-hover");
el.classList.add("is-flag");
el.setAttribute("data-you-changed-this", "yes");

Then open this page in another tab and inspect the element — the class and the custom attribute will be there too.

Same trick for programmatic child changes:

const el = document.querySelector("#pg-mirror-editable-list");
const li = document.createElement("li");
li.textContent = "added via console";
el.appendChild(li);

Anything a MutationObserver would fire for — attribute additions and removals, childList inserts, characterData changes — can-mirror sends across. That’s the whole capability.

  • Scroll position on nested containers — scrollTop/scrollLeft aren’t DOM mutations, so the observer doesn’t catch them.
  • Media playback<video>/<audio> currentTime isn’t attribute-driven.
  • Canvas / WebGL — pixel changes don’t emit mutations.
  • File inputs<input type="file">’s selected file isn’t serializable across the wire.

For these, use element data with can-play and explicitly sync the bit you care about.

  • can-mirror: the DOM shape is the source of truth. Forms, collaborative scratchpads, shared to-do checkboxes, native UI elements.
  • can-play: you want a custom data shape with your own render function. Chat messages, multi-dimensional state, counts, lists where the visual is computed.

Rule of thumb: if your element would work correctly with just contenteditable and form inputs, can-mirror is the least-code path. The moment you reach for JSON.stringify, switch to can-play.