Page status board
Each reader picks a status, and everyone sees each person’s status next to their name. A status is page presence: everyone can see it, only you can change yours, and it’s gone when you leave.
- 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>Page status board</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; }
.picker {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.picker button {
padding: 0.35rem 0.75rem;
border: 1.5px solid #1c1c1c;
border-radius: 999px;
background: white;
font: inherit;
cursor: pointer;
}
/* Your current status is highlighted. */
.picker button[aria-pressed="true"] {
background: #e8a63a;
}
.people {
display: grid;
gap: 0.35rem;
margin: 1rem 0 0;
padding: 0;
list-style: none;
}
.people li {
display: flex;
align-items: center;
gap: 0.55rem;
padding: 0.4rem 0.6rem;
border: 1.5px solid #1c1c1c;
border-radius: 6px;
background: white;
}
.people .dot {
width: 14px;
height: 14px;
border: 1.5px solid #1c1c1c;
border-radius: 50%;
}
.people .status {
margin-left: auto;
color: #6a6a66;
}
</style>
</head>
<body>
<main>
<div id="picker" class="picker"></div>
<ul id="people" class="people"></ul>
</main>
<script type="module">
import { playhtml } from "https://unpkg.com/playhtml@latest";
const STATUSES = ["reading", "coding", "taking a break"];
const picker = document.getElementById("picker");
const list = document.getElementById("people");
// playhtml.people is available once playhtml has connected.
playhtml.ready.then(() => {
const { people } = playhtml;
for (const status of STATUSES) {
const button = document.createElement("button");
button.textContent = status;
button.addEventListener("click", () => {
// Picking your current status again clears it (null removes the key).
const mine = people.me.presence.status;
people.setMyPresence("status", mine === status ? null : status);
});
picker.append(button);
}
// Calls back now, then when someone joins, leaves, renames, or changes
// their status.
people.onChange("status", (everyone) => {
const mine = people.me.presence.status;
for (const button of picker.children) {
button.setAttribute("aria-pressed", String(button.textContent === mine));
}
list.replaceChildren(
...everyone.map((person) => {
const row = document.createElement("li");
const dot = document.createElement("span");
dot.className = "dot";
dot.style.background = person.color;
const name = document.createElement("span");
name.textContent = (person.name ?? "anonymous") + (person.isMe ? " (you)" : "");
const status = document.createElement("span");
status.className = "status";
status.textContent = person.presence.status ?? "…";
row.append(dot, name, status);
return row;
}),
);
});
});
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, usePeople } from "@playhtml/react";
const STATUSES = ["reading", "coding", "taking a break"];
function StatusBoard() {
// Re-renders when someone joins, leaves, renames, or changes their status.
const { everyone, me, setMyPresence } = usePeople("status");
const mine = me.presence.status;
return (
<div>
<div className="picker">
{STATUSES.map((status) => (
<button
key={status}
aria-pressed={mine === status}
// Picking your current status again clears it (null removes the key).
onClick={() => setMyPresence("status", mine === status ? null : status)}
>
{status}
</button>
))}
</div>
<ul className="people">
{everyone.map((person) => (
<li key={person.pid}>
<span className="dot" style={{ background: person.color }} />
<span>
{person.name ?? "anonymous"}
{person.isMe ? " (you)" : ""}
</span>
<span className="status">{String(person.presence.status ?? "…")}</span>
</li>
))}
</ul>
</div>
);
}
export default function App() {
return (
<PlayProvider initOptions={{ developmentMode: true }}>
<main>
<StatusBoard />
</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; }
.picker {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.picker button {
padding: 0.35rem 0.75rem;
border: 1.5px solid #1c1c1c;
border-radius: 999px;
background: white;
font: inherit;
cursor: pointer;
}
/* Your current status is highlighted. */
.picker button[aria-pressed="true"] {
background: #e8a63a;
}
.people {
display: grid;
gap: 0.35rem;
margin: 1rem 0 0;
padding: 0;
list-style: none;
}
.people li {
display: flex;
align-items: center;
gap: 0.55rem;
padding: 0.4rem 0.6rem;
border: 1.5px solid #1c1c1c;
border-radius: 6px;
background: white;
}
.people .dot {
width: 14px;
height: 14px;
border: 1.5px solid #1c1c1c;
border-radius: 50%;
}
.people .status {
margin-left: auto;
color: #6a6a66;
}
`}</style>
</PlayProvider>
);
} Set your status
Section titled “Set your status”setMyPresence(key, value) sets one key of your page presence. Pass null to clear it:
playhtml.people.setMyPresence("status", "reading");
playhtml.people.setMyPresence("status", null);
Show everyone’s
Section titled “Show everyone’s”onChange("status", callback) calls back now, then when someone joins, leaves, renames, or changes their status. Each person’s status is at person.presence.status:
playhtml.people.onChange("status", (everyone) => {
for (const person of everyone) {
console.log(person.name, person.presence.status);
}
});
In React, usePeople("status") re-renders on the same changes.
See page presence for the full API.