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.
Mirror boundary
Section titled “Mirror boundary”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-hoveranddata-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.
:hover
Section titled “:hover”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>
:focus
Section titled “:focus”Click the input and the blue outline shows for every client until you blur.
Text input
Section titled “Text input”Type and the value replicates.
Textarea
Section titled “Textarea”Checkbox
Section titled “Checkbox”Radio buttons
Section titled “Radio buttons”Select (dropdown)
Section titled “Select (dropdown)”Range slider
Section titled “Range slider”Color picker
Section titled “Color picker”Details / summary
Section titled “Details / summary”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.
contenteditable
Section titled “contenteditable”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
Mixed form inputs
Section titled “Mixed form inputs”A single can-mirror wrapping a form with heterogeneous inputs, all of them sync together.
Play from devtools
Section titled “Play from devtools”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>
What doesn’t mirror
Section titled “What doesn’t mirror”- Arbitrary nested DOM mutations: a parent
can-mirrordoes not recursively own changes inside descendant elements. Putcan-mirroron the nested element that should sync. - Scroll position on nested containers:
scrollTop/scrollLeftaren’t DOM mutations, so the observer doesn’t catch them. - Media playback:
<video>/<audio>currentTimeisn’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.
When to reach for can-mirror vs can-play
Section titled “When to reach for can-mirror vs can-play”- 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.