Skip to content

Custom cursors

Everyone elseโ€™s cursor shows up as an emoji in a ring of their color. Each person gets the same emoji on every screen. Open this page in a second window to see it.

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

Both versions use the same shared data and behavior. The live playground runs the Vanilla HTML version.

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>Custom cursors</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; }
    .critter-pad {
      display: grid;
      gap: 0.5rem;
      place-items: center;
      width: 100%;
      min-height: 14rem;
      border: 1.5px dashed #65795d;
      border-radius: 8px;
      text-align: center;
    }
    .critter-pad p { margin: 0; max-width: 22rem; line-height: 1.5; }
    /* A custom cursor: an emoji in a ring of the person's color. */
    .critter {
      display: grid;
      place-items: center;
      width: 32px;
      height: 32px;
      border: 3px solid var(--color);
      border-radius: 50%;
      background: white;
      font-size: 18px;
      transform: translate(-50%, -50%);
    }
  </style>
</head>
<body>
  <main class="critter-pad">
    <p>You're <span id="my-critter"></span> to everyone here.</p>
    <p>Open this page in a second window and move your mouse in both.</p>
  </main>

  <script type="module">
    import { playhtml } from "https://unpkg.com/playhtml@latest";

    const CRITTERS = ["๐Ÿ", "๐Ÿž", "๐Ÿฆ‹", "๐ŸŒ", "๐Ÿธ", "๐Ÿฆ†"];

    function critterFor(pid) {
      let hash = 0;
      for (const char of pid) hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
      return CRITTERS[hash % CRITTERS.length];
    }

    // Your own pointer as a CSS cursor: the same emoji and ring others see.
    function critterCursor(emoji, color) {
      const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32">
        <circle cx="16" cy="16" r="13" fill="white" stroke="${color}" stroke-width="3" />
        <text x="16" y="21.5" font-size="15" text-anchor="middle">${emoji}</text>
      </svg>`;
      return `url("data:image/svg+xml,${encodeURIComponent(svg)}") 16 16, auto`;
    }

    // Called once for each other person's cursor. Fill in `element` and
    // return it, or return null to keep the default arrow. playhtml moves the
    // element and adds the person's name tag.
    function renderCursor(pid, element) {
      const person = playhtml.people.everyone.find((p) => p.pid === pid);
      const critter = document.createElement("div");
      critter.className = "critter";
      critter.textContent = critterFor(pid);
      critter.style.setProperty("--color", person?.color ?? "#1c1c1c");
      element.replaceChildren(critter);
      return element;
    }

    await playhtml.init({
      developmentMode: true,
      cursors: {
        enabled: true,
        onCustomCursorRender: renderCursor,
      },
    });

    // onCustomCursorRender only draws other people. Draw your own pointer
    // with CSS so you see what everyone else sees.
    const me = playhtml.people.me;
    document.getElementById("my-critter").textContent = critterFor(me.pid);
    document.body.style.cursor = critterCursor(critterFor(me.pid), me.color);
  </script>
</body>
</html>

Pass onCustomCursorRender with your cursor options. playhtml calls it once per person with their pid and an empty element. Fill the element and return it. playhtml positions it and adds the name tag:

function renderCursor(pid, element) {
  const person = playhtml.people.everyone.find((p) => p.pid === pid);
  const critter = document.createElement("div");
  critter.className = "critter";
  critter.textContent = critterFor(pid);
  critter.style.setProperty("--color", person?.color ?? "#1c1c1c");
  element.replaceChildren(critter);
  return element;
}

await playhtml.init({
  cursors: { enabled: true, onCustomCursorRender: renderCursor },
});

Return null to keep the default arrow for someone.

onCustomCursorRender only draws other people. To show your own pointer the same way, set a CSS cursor image on the page after init:

const me = playhtml.people.me;
document.body.style.cursor = critterCursor(critterFor(me.pid), me.color);

critterCursor draws the emoji and ring into an SVG data URL. A CSS cursor moves with no lag, but it has to be an image, so it canโ€™t hold a live element.

critterFor hashes the pid into an index, so a person keeps one emoji across every screen and every visit without saving anything.

See Cursors for the other cursor options.