Shared page color
The page’s background color is shared. Pick a swatch in either corner and the whole page changes color for everyone. The color is saved, so it’s still there for the next visitor.
- 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 page color</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; }
body {
display: grid;
grid-template-rows: auto 1fr auto;
place-items: stretch;
padding: 0.75rem;
transition: background 300ms;
}
.corner { display: flex; gap: 0.4rem; }
.corner.top { justify-content: flex-start; }
.corner.bottom { justify-content: flex-end; }
.corner button {
width: 26px;
height: 26px;
border: 1.5px solid #1c1c1c;
border-radius: 50%;
background: var(--swatch);
cursor: pointer;
}
.corner button:hover { box-shadow: 2px 2px 0 #1c1c1c; }
.middle { display: grid; gap: 0.6rem; place-content: center; text-align: center; }
.middle p { margin: 0; max-width: 20rem; line-height: 1.5; }
.readout {
justify-self: center;
padding: 0.2rem 0.5rem;
border: 1px solid #1c1c1c;
border-radius: 4px;
background: white;
font: 0.8rem ui-monospace, monospace;
}
</style>
</head>
<body>
<nav class="corner top">
<button data-color="#f4efe5" style="--swatch: #f4efe5" aria-label="Paint the page #f4efe5"></button>
<button data-color="#f3cf58" style="--swatch: #f3cf58" aria-label="Paint the page #f3cf58"></button>
<button data-color="#c9d8bf" style="--swatch: #c9d8bf" aria-label="Paint the page #c9d8bf"></button>
<button data-color="#f2c4ce" style="--swatch: #f2c4ce" aria-label="Paint the page #f2c4ce"></button>
<button data-color="#bcd4e6" style="--swatch: #bcd4e6" aria-label="Paint the page #bcd4e6"></button>
</nav>
<main id="page-color" class="middle">
<p>Pick a color in either corner. It paints the whole page for everyone.</p>
<code id="readout" class="readout"></code>
</main>
<nav class="corner bottom">
<button data-color="#f4efe5" style="--swatch: #f4efe5" aria-label="Paint the page #f4efe5"></button>
<button data-color="#f3cf58" style="--swatch: #f3cf58" aria-label="Paint the page #f3cf58"></button>
<button data-color="#c9d8bf" style="--swatch: #c9d8bf" aria-label="Paint the page #c9d8bf"></button>
<button data-color="#f2c4ce" style="--swatch: #f2c4ce" aria-label="Paint the page #f2c4ce"></button>
<button data-color="#bcd4e6" style="--swatch: #bcd4e6" aria-label="Paint the page #bcd4e6"></button>
</nav>
<script type="module">
import { playhtml } from "https://unpkg.com/playhtml@latest";
await playhtml.init({ developmentMode: true });
// The page's background color is one value in a page data channel.
// No element owns it: any code on the page can read or write it by name.
const pageColor = playhtml.createPageData("page-color", "#f4efe5");
const readout = document.getElementById("readout");
function paint(color) {
document.body.style.background = color;
readout.textContent = `"page-color": "${color}"`;
}
// Both corners' swatches write the same channel.
for (const swatch of document.querySelectorAll("[data-color]")) {
swatch.addEventListener("click", () => {
pageColor.setData(swatch.dataset.color);
});
}
pageColor.onUpdate(paint);
paint(pageColor.getData());
</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 { useEffect } from "react";
import { PlayProvider, usePageData } from "@playhtml/react";
const SWATCHES = ["#f4efe5","#f3cf58","#c9d8bf","#f2c4ce","#bcd4e6"];
// The page's background color is one value in a page data channel. No
// component owns it: any component can read or write it by name.
function usePageColor() {
return usePageData("page-color", "#f4efe5");
}
function Swatches({ where }: { where: "top" | "bottom" }) {
const [, setColor] = usePageColor();
return (
<nav className={`corner ${where}`}>
{SWATCHES.map((color) => (
<button
key={color}
onClick={() => setColor(color)}
style={{ background: color }}
aria-label={`Paint the page ${color}`}
/>
))}
</nav>
);
}
function PageColor() {
const [color] = usePageColor();
// Painting the body reads the channel; it never writes it.
useEffect(() => {
document.body.style.background = color;
}, [color]);
return (
<>
<Swatches where="top" />
<div className="middle">
<p>Pick a color in either corner. It paints the whole page for everyone.</p>
<code className="readout">"page-color": "{color}"</code>
</div>
<Swatches where="bottom" />
</>
);
}
export default function App() {
return (
<PlayProvider initOptions={{ developmentMode: true }}>
<main>
<PageColor />
</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; }
body {
display: grid;
grid-template-rows: auto 1fr auto;
place-items: stretch;
padding: 0.75rem;
transition: background 300ms;
}
.corner { display: flex; gap: 0.4rem; }
.corner.top { justify-content: flex-start; }
.corner.bottom { justify-content: flex-end; }
.corner button {
width: 26px;
height: 26px;
border: 1.5px solid #1c1c1c;
border-radius: 50%;
background: var(--swatch);
cursor: pointer;
}
.corner button:hover { box-shadow: 2px 2px 0 #1c1c1c; }
.middle { display: grid; gap: 0.6rem; place-content: center; text-align: center; }
.middle p { margin: 0; max-width: 20rem; line-height: 1.5; }
.readout {
justify-self: center;
padding: 0.2rem 0.5rem;
border: 1px solid #1c1c1c;
border-radius: 4px;
background: white;
font: 0.8rem ui-monospace, monospace;
}
`}</style>
</PlayProvider>
);
} The page owns the color
Section titled “The page owns the color”The color doesn’t belong to any element, so it lives in a page data channel
named "page-color". Create it after playhtml.init resolves. A channel can
hold a plain value like a string:
await playhtml.init();
const pageColor = playhtml.createPageData("page-color", "#f4efe5");
Write from either corner
Section titled “Write from either corner”The top and bottom swatches are separate elements. Both write the same
channel by name, and one onUpdate paints the page:
swatch.addEventListener("click", () => {
pageColor.setData(swatch.dataset.color);
});
pageColor.onUpdate((color) => {
document.body.style.background = color;
});
If two people pick at the same moment, the last pick wins. That’s what you want for one color.
See Page-level data for the full channel API.