Skip to content

Growing mirrored list

can-mirror shares direct child additions and removals. Put it on the list, then change that list with normal DOM methods.

Live exampleRoom connecting…
Remix
Test it with another person
  1. Keep this page open in your normal window.
  2. Copy the private-window link, then paste it into a private or incognito window.
  3. Interact in either window and watch the other one update.

Copy the code

The live playground and this source use the same Vanilla HTML recipe.

Save this as index.html, or open it in the playground to test and change it.

Remix

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Growing mirrored list</title>
  <style>
    :root {
      color: #1c1c1c;
      background: #f4efe5;
      font-family: ui-sans-serif, system-ui, sans-serif;
    }
    * { box-sizing: border-box; }
    body { display: grid; min-height: 100vh; margin: 0; padding: 1.5rem; place-items: center; }
    main { width: min(34rem, 100%); }
    h1 { margin: 0 0 0.35rem; font-size: clamp(2rem, 8vw, 3.5rem); line-height: 1; }
    .intro { margin: 0 0 1.25rem; line-height: 1.5; }
    .panel {
      padding: 1rem;
      border: 2px solid #1c1c1c;
      background: #ebe4d5;
      box-shadow: 5px 5px 0 #1c1c1c;
    }
    ul {
      min-height: 7rem;
      margin: 0 0 1rem;
      padding: 0.75rem 0.75rem 0.75rem 2rem;
      border: 1px solid #1c1c1c;
      background: #fffdf8;
    }
    li + li { margin-top: 0.35rem; }
    button {
      padding: 0.65rem 0.9rem;
      border: 2px solid #1c1c1c;
      background: #f3cf58;
      box-shadow: 3px 3px 0 #1c1c1c;
      cursor: pointer;
      font: 700 1rem/1 ui-sans-serif, system-ui, sans-serif;
    }
    button:active { translate: 3px 3px; box-shadow: none; }
  </style>
</head>
<body>
  <main>
    <h1>Shared list</h1>
    <p class="intro">Add an item. The direct child appears for everyone.</p>
    <section class="panel">
      <ul id="growing-list" can-mirror>
        <li>first</li>
      </ul>
      <button id="add-list-item" type="button">add entry</button>
    </section>
  </main>

  <script type="module">
    import { playhtml } from "playhtml";

    const list = document.getElementById("growing-list");
    const addButton = document.getElementById("add-list-item");

    addButton.addEventListener("click", () => {
      const item = document.createElement("li");
      item.textContent = new Date().toLocaleTimeString();
      list.appendChild(item);
    });

    await playhtml.init({ developmentMode: true });
  </script>
</body>
</html>

Create an element and append it to the mirrored list:

const item = document.createElement("li");
item.textContent = new Date().toLocaleTimeString();
list.appendChild(item);

The list owns the child change because the new li is a direct child. Mutations deeper inside a child need their own can-mirror boundary.

Add entries from both windows. Each new item should appear in both lists and remain after reload.

See the can-mirror playground for nested elements, forms, contenteditable, focus, and hover behavior.