Skip to content

can-mirror playground

can-mirror syncs the element you put it on. That includes attributes on that element, its direct child list, and form or contenteditable state that surfaces through input/change events.

It does not recursively watch arbitrary descendant DOM mutations. If a nested list, card, or editor should own its own changes, put can-mirror on that nested element too and give it a stable id.

This page hosts the common edge cases, one section each. Open it in two browser windows side by side and poke at each demo.

can-mirror updates:

  • attributes on the mirrored element itself, including class and style changes;
  • direct child additions, removals, and reorders on the mirrored element itself;
  • form state inside the mirrored element when inputs fire input/change events;
  • contenteditable changes when the browser reports them through input events;
  • hover and focus awareness through data-playhtml-hover and data-playhtml-focus.

can-mirror does not update:

  • class, attribute, or text changes on arbitrary descendants inside the mirrored element;
  • additions or removals inside a nested list unless that nested list has its own can-mirror;
  • bare text-node edits like el.firstChild.textContent = "..." unless they happen through an input/contenteditable event.

The synced data-playhtml-hover attribute lets CSS target the hover state from across the network. Hover over the box in one tab and 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 and the blue outline shows for every client until you blur.

Type and the value replicates.



Expand and collapse, and the native open attribute mirrors automatically.

Click to expand

Hidden until the details element is opened. The open attribute lives on the mirrored element, so it syncs automatically.

Type, paste, and delete inside the contenteditable element. Browser input events sync the editable contents to 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 for attributes and direct child-list changes on the mirrored element. It doesn’t care whether those changes come 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 direct child changes:

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

Attribute additions/removals on the mirrored element and direct child additions/removals on the mirrored element send across.

If you mutate a descendant inside a mirrored parent, that nested mutation does not sync by itself. Put can-mirror on the nested element that owns the changing child list.

<div id="panel">
  <ul can-mirror id="panel-items">
    <li>First item</li>
  </ul>
</div>
  • Arbitrary nested DOM mutations: a parent can-mirror does not recursively own changes inside descendant elements. Put can-mirror on the nested element that should sync.
  • 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.