Shared click counter
This counter stores { count: 0 } as shared data. Each click mutates the
current count so concurrent clicks merge without replacing newer values.
Test it with another person
- Keep this page open in your normal window.
- Copy the private-window link, then paste it into a private or incognito window.
- 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.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Shared click counter</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(40rem, 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; }
main { text-align: center; }
.counter {
display: grid;
width: 100%;
gap: 0.45rem;
padding: 1.4rem;
border: 2px solid #1c1c1c;
background: #f2a7b7;
box-shadow: 5px 5px 0 #1c1c1c;
cursor: pointer;
font: 700 1rem/1 ui-sans-serif, system-ui, sans-serif;
}
.counter strong { font-size: 4rem; line-height: 1; }
.counter:active { translate: 5px 5px; box-shadow: none; }
</style>
</head>
<body>
<main>
<h1>Shared counter</h1>
<p class="intro">Click the counter. Every connected browser increments the same value.</p>
<button id="shared-counter" class="counter" type="button" can-play>
<span aria-hidden="true">heart</span>
<strong data-count>0</strong>
<span>shared clicks</span>
</button>
</main>
<script type="module">
import { playhtml } from "playhtml";
const counter = document.getElementById("shared-counter");
counter.defaultData = { count: 0 };
counter.onClick = (_event, { setData }) => {
setData((draft) => {
draft.count += 1;
});
};
counter.updateElement = ({ element, data }) => {
element.querySelector("[data-count]").textContent = String(data.count);
};
await playhtml.init({ developmentMode: true });
</script>
</body>
</html>
Start with a React + TypeScript project, install the packages below,
then replace src/App.tsx with the component.
npm install playhtml @playhtml/react // ABOUTME: Renders one persistent counter shared by every connected browser.
// ABOUTME: Increments the current value with a merge-safe mutator write.
import { PlayProvider, withSharedState } from "@playhtml/react";
type CounterData = { count: number };
const SharedCounter = withSharedState<CounterData>(
{
id: "shared-counter",
defaultData: { count: 0 },
},
({ data, setData }) => (
<button
id="shared-counter"
className="counter"
type="button"
onClick={() => {
setData((draft) => {
draft.count += 1;
});
}}
>
<span aria-hidden="true">heart</span>
<strong>{data.count}</strong>
<span>shared clicks</span>
</button>
),
);
export default function App() {
return (
<PlayProvider initOptions={{ developmentMode: true }}>
<main>
<h1>Shared counter</h1>
<p>Click the counter. Every connected browser increments the same value.</p>
<SharedCounter />
</main>
<style>{`
:root { color: #1c1c1c; background: #f4efe5; font-family: system-ui, sans-serif; }
* { box-sizing: border-box; }
body { margin: 0; }
#root { display: grid; min-height: 100vh; padding: 1.5rem; place-items: center; }
main { width: min(30rem, 100%); text-align: center; }
h1 { margin: 0 0 0.35rem; font-size: clamp(2rem, 9vw, 3.5rem); }
p { margin: 0 0 1.25rem; line-height: 1.5; }
.counter {
display: grid;
width: 100%;
gap: 0.45rem;
padding: 1.4rem;
border: 2px solid #1c1c1c;
background: #f2a7b7;
box-shadow: 5px 5px 0 #1c1c1c;
cursor: pointer;
font: 700 1rem/1 system-ui, sans-serif;
}
.counter strong { font-size: 4rem; line-height: 1; }
.counter:active { translate: 5px 5px; box-shadow: none; }
`}</style>
</PlayProvider>
);
} Increment from the click handler
Section titled “Increment from the click handler”Write from the user event:
counter.onClick = (_event, { setData }) => {
setData((draft) => {
draft.count += 1;
});
};
Don’t rebuild the value from rendered text. The rendered value may be behind a remote update.
Render the current count
Section titled “Render the current count”updateElement reads shared data without writing it:
counter.updateElement = ({ element, data }) => {
element.querySelector("[data-count]").textContent = String(data.count);
};
See Data essentials for mutator and replacement writes.