Online presence indicator
Each reader on the page gets a dot. Yours glows. Pick a color and everyone sees your dot change. Close a window and its dot disappears.
- 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>Online presence indicator</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; }
.dots {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.dot {
width: 42px;
height: 42px;
border: 1.5px solid #1c1c1c;
border-radius: 50%;
}
/* Your own dot glows. */
.dot.me {
box-shadow: 0 0 0 3px white, 0 0 0 5px #1c1c1c,
0 0 22px 6px rgba(245, 169, 15, 0.7);
}
</style>
</head>
<body>
<main>
<div id="online">
<div class="dots"></div>
<p class="count"></p>
</div>
<label>
your color
<input id="my-color" type="color" value="#4a6a3e" />
</label>
</main>
<script type="module">
import { playhtml } from "https://unpkg.com/playhtml@latest";
// Each person here has a presence on this element: a value everyone can
// see, gone when they leave. Yours starts at `defaultPresence`, and
// `everyone` lists each person (name, color, isMe) with theirs.
const online = playhtml.register("online", {
defaultPresence: { color: "#4a6a3e" },
update: ({ element, everyone }) => {
element.querySelector(".dots").replaceChildren(
...everyone.map((person) => {
const dot = document.createElement("span");
dot.className = person.isMe ? "dot me" : "dot";
dot.style.background = person.presence.color;
return dot;
}),
);
const people = everyone.length === 1 ? "person" : "people";
element.querySelector(".count").textContent =
`${everyone.length} ${people} here`;
},
});
document.getElementById("my-color").addEventListener("input", (event) => {
online.setMyPresence({ color: event.target.value });
});
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 import { PlayProvider, withSharedState } from "@playhtml/react";
const DEFAULT_COLOR = "#4a6a3e";
// Each person here has a presence on this element: a value everyone can
// see, gone when they leave. Yours starts at `defaultPresence`, and
// `everyone` lists each person (name, color, isMe) with theirs.
const OnlineIndicator = withSharedState(
{ id: "online", defaultData: {}, defaultPresence: { color: DEFAULT_COLOR } },
({ everyone, me, setMyPresence }) => (
<div>
<div className="dots">
{everyone.map((person) => (
<span
key={person.pid}
className={person.isMe ? "dot me" : "dot"}
style={{ background: person.presence.color }}
/>
))}
</div>
<p>
{everyone.length} {everyone.length === 1 ? "person" : "people"} here
</p>
<label>
your color
<input
type="color"
value={me.presence.color}
onChange={(event) => setMyPresence({ color: event.target.value })}
/>
</label>
</div>
),
);
export default function App() {
return (
<PlayProvider initOptions={{ developmentMode: true }}>
<main>
<OnlineIndicator />
</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; }
.dots {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.dot {
width: 42px;
height: 42px;
border: 1.5px solid #1c1c1c;
border-radius: 50%;
}
/* Your own dot glows. */
.dot.me {
box-shadow: 0 0 0 3px white, 0 0 0 5px #1c1c1c,
0 0 22px 6px rgba(245, 169, 15, 0.7);
}
`}</style>
</PlayProvider>
);
} Presence per reader
Section titled “Presence per reader”Each reader has a presence on this element: a value everyone can see, never
saved, gone when they leave. Yours starts at defaultPresence. everyone lists
everyone here right now, including you, each with their name and color and
their presence on this element:
everyone.map((person) => person.presence.color);
person.isMe marks your own dot.
Set your value
Section titled “Set your value”Call setMyPresence from a user event. It sets your whole value and reaches everyone
else right away:
online.setMyPresence({ color: event.target.value });
See people and presence for page-wide presence and cursors.