Send a rain event
Click the hydrant and it rains for everyone on the page for a few seconds. Someone who opens the page afterward sees nothing, because events are never stored.
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>Send a rain event</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; }
.hydrant {
width: 120px;
height: 150px;
border: 0;
background: url("https://playhtml.fun/docs/fire-hydrant.png") center / contain
no-repeat;
cursor: pointer;
}
.rain {
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: none;
}
.rain::before {
content: "";
position: absolute;
inset: 0;
background: url("https://media.giphy.com/media/Wmp1EOzVybWd13s5DB/giphy.gif")
0 0 / 200px;
animation: rain 6s ease-in-out forwards;
}
.cloud {
position: absolute;
top: 10px;
width: 150px;
height: 80px;
background: url("https://playhtml.fun/docs/rain-cloud.webp") center / contain
no-repeat;
animation: cloud 5.5s ease-out both;
}
@keyframes rain {
0%, 15% { opacity: 0; }
30%, 85% { opacity: 0.7; }
100% { opacity: 0; }
}
@keyframes cloud {
0% { opacity: 0; transform: translateY(-120px); }
20%, 80% { opacity: 0.9; transform: none; }
100% { opacity: 0; transform: translateY(-120px); }
}
</style>
</head>
<body>
<main>
<button id="hydrant" class="hydrant" aria-label="Summon rain"></button>
</main>
<script type="module">
import { playhtml } from "https://unpkg.com/playhtml@latest";
// Fade in rain under a row of five clouds, then clean up.
function makeItRain() {
const rain = document.createElement("div");
rain.className = "rain";
for (let i = 0; i < 5; i++) {
const cloud = document.createElement("div");
cloud.className = "cloud";
cloud.style.left = `${i * 20}%`;
cloud.style.animationDelay = `${i * 150}ms`;
rain.append(cloud);
}
document.body.append(rain);
setTimeout(() => rain.remove(), 6000);
}
document.getElementById("hydrant").addEventListener("click", () => {
playhtml.dispatchPlayEvent({ type: "rain" });
});
await playhtml.init({
developmentMode: true,
events: {
rain: { type: "rain", onEvent: makeItRain },
},
});
</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, usePlayContext } from "@playhtml/react";
// Fade in rain under a row of five clouds, then clean up.
function makeItRain() {
const rain = document.createElement("div");
rain.className = "rain";
for (let i = 0; i < 5; i++) {
const cloud = document.createElement("div");
cloud.className = "cloud";
cloud.style.left = `${i * 20}%`;
cloud.style.animationDelay = `${i * 150}ms`;
rain.append(cloud);
}
document.body.append(rain);
setTimeout(() => rain.remove(), 6000);
}
function RainHydrant() {
const {
registerPlayEventListener,
removePlayEventListener,
dispatchPlayEvent,
} = usePlayContext();
// Listen while mounted. Without the cleanup, every remount adds a listener.
useEffect(() => {
const id = registerPlayEventListener("rain", { onEvent: makeItRain });
return () => removePlayEventListener("rain", id);
}, []);
return (
<button
className="hydrant"
aria-label="Summon rain"
onClick={() => dispatchPlayEvent({ type: "rain" })}
/>
);
}
export default function App() {
return (
<PlayProvider initOptions={{ developmentMode: true }}>
<main>
<RainHydrant />
</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; }
.hydrant {
width: 120px;
height: 150px;
border: 0;
background: url("https://playhtml.fun/docs/fire-hydrant.png") center / contain
no-repeat;
cursor: pointer;
}
.rain {
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: none;
}
.rain::before {
content: "";
position: absolute;
inset: 0;
background: url("https://media.giphy.com/media/Wmp1EOzVybWd13s5DB/giphy.gif")
0 0 / 200px;
animation: rain 6s ease-in-out forwards;
}
.cloud {
position: absolute;
top: 10px;
width: 150px;
height: 80px;
background: url("https://playhtml.fun/docs/rain-cloud.webp") center / contain
no-repeat;
animation: cloud 5.5s ease-out both;
}
@keyframes rain {
0%, 15% { opacity: 0; }
30%, 85% { opacity: 0.7; }
100% { opacity: 0; }
}
@keyframes cloud {
0% { opacity: 0; transform: translateY(-120px); }
20%, 80% { opacity: 0.9; transform: none; }
100% { opacity: 0; transform: translateY(-120px); }
}
`}</style>
</PlayProvider>
);
} Listen, then dispatch
Section titled “Listen, then dispatch”Register the event with playhtml.init, then dispatch it from a click. Every
connected browser, including yours, runs onEvent:
playhtml.init({
events: {
rain: { type: "rain", onEvent: makeItRain },
},
});
hydrant.addEventListener("click", () => {
playhtml.dispatchPlayEvent({ type: "rain" });
});
The rain itself is plain DOM and CSS animation that removes itself after six seconds.
See Events for when to use events instead of shared data.