Live reactions
Click an emoji and it floats up for everyone on the page. Reactions are events, so they show up only for people here right now and 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>Live reactions</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; }
.reactions {
position: relative;
width: min(24rem, 100%);
height: 180px;
overflow: hidden;
}
.burst {
position: absolute;
bottom: 0;
font-size: 1.65rem;
pointer-events: none;
animation: float-up 2.5s ease-out forwards;
}
@keyframes float-up {
to { opacity: 0; transform: translateY(-140px) scale(0.8); }
}
</style>
</head>
<body>
<main id="reactions" class="reactions">
<button data-emoji="❤️">❤️</button>
<button data-emoji="✨">✨</button>
<button data-emoji="🔥">🔥</button>
<button data-emoji="🎉">🎉</button>
</main>
<script type="module">
import { playhtml } from "https://unpkg.com/playhtml@latest";
const stage = document.getElementById("reactions");
// Float one emoji up from a random spot, then remove it.
function burst({ emoji }) {
const el = document.createElement("span");
el.className = "burst";
el.textContent = emoji;
el.style.left = `${Math.random() * 90}%`;
el.addEventListener("animationend", () => el.remove());
stage.append(el);
}
for (const button of stage.querySelectorAll("button")) {
button.addEventListener("click", () => {
playhtml.dispatchPlayEvent({
type: "reaction",
eventPayload: { emoji: button.dataset.emoji },
});
});
}
await playhtml.init({
developmentMode: true,
events: {
reaction: { type: "reaction", onEvent: burst },
},
});
</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, useState } from "react";
import { PlayProvider, usePlayContext } from "@playhtml/react";
const EMOJIS = ["❤️", "✨", "🔥", "🎉"];
type Burst = { id: string; emoji: string; left: number };
function LiveReactions() {
const {
registerPlayEventListener,
removePlayEventListener,
dispatchPlayEvent,
} = usePlayContext();
const [bursts, setBursts] = useState<Burst[]>([]);
// Each reaction becomes a burst that removes itself once it floats away.
useEffect(() => {
const id = registerPlayEventListener("reaction", {
onEvent: ({ emoji }: { emoji: string }) => {
const left = Math.random() * 90;
setBursts((all) => [...all, { id: crypto.randomUUID(), emoji, left }]);
},
});
return () => removePlayEventListener("reaction", id);
}, []);
return (
<div className="reactions">
{EMOJIS.map((emoji) => (
<button
key={emoji}
onClick={() =>
dispatchPlayEvent({ type: "reaction", eventPayload: { emoji } })
}
>
{emoji}
</button>
))}
{bursts.map((burst) => (
<span
key={burst.id}
className="burst"
style={{ left: `${burst.left}%` }}
onAnimationEnd={() =>
setBursts((all) => all.filter((b) => b.id !== burst.id))
}
>
{burst.emoji}
</span>
))}
</div>
);
}
export default function App() {
return (
<PlayProvider initOptions={{ developmentMode: true }}>
<main>
<LiveReactions />
</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; }
.reactions {
position: relative;
width: min(24rem, 100%);
height: 180px;
overflow: hidden;
}
.burst {
position: absolute;
bottom: 0;
font-size: 1.65rem;
pointer-events: none;
animation: float-up 2.5s ease-out forwards;
}
@keyframes float-up {
to { opacity: 0; transform: translateY(-140px) scale(0.8); }
}
`}</style>
</PlayProvider>
);
} Send a payload
Section titled “Send a payload”eventPayload travels with the event, and onEvent receives it:
playhtml.dispatchPlayEvent({
type: "reaction",
eventPayload: { emoji: "🎉" },
});
// Every browser on the page, including yours, runs this.
function burst({ emoji }) {
// float the emoji up
}
playhtml.init({
events: {
reaction: { type: "reaction", onEvent: burst },
},
});
Each burst is a CSS animation that removes itself when it ends, so nothing piles up.
See Events for when to use events instead of shared data.