Live signup social proof
The little "Sarah from Berlin just started a trial" toast is one of the most copied growth patterns on the web, and by now most visitors are immune to it. Putting it on a globe fixes the two things wrong with the toast: it is not in the way, and it accumulates into a picture instead of disappearing.
The code
"use client";
import { useEffect, useRef } from "react";
import { Globe } from "canvas-globe/react";
export function SocialProofGlobe({ cities, total }) {
const globe = useRef(null);
useEffect(() => {
const source = new EventSource("/api/signups/stream");
source.onmessage = (event) => {
const { name, city, lat, lon } = JSON.parse(event.data);
globe.current?.ping({
lat,
lon,
emoji: "✨",
label: `${name} just signed up in ${city}`,
});
};
return () => source.close();
}, []);
return (
<Globe
ref={globe}
preset="hologram"
rotateSpeed={0.05}
markers={cities}
markerStyle="dot"
counter={{ value: total, label: "signups all time" }}
showViewer={{ label: "You", live: true }}
style={{ maxWidth: 520 }}
/>
);
}
Pings versus markers
This is the distinction that makes or breaks the effect.
| Marker | Ping | |
|---|---|---|
| Represents | a place that exists | an event that happened |
| Lifetime | until you remove it | ~1.4 s, then gone |
| Effect of many | a denser map | a busier map |
Signups are events. If you push each one into setMarkers, the map only ever grows and after a
minute it is a solid smear. ping() fires an expanding ring that fades, so the map stays readable
while still feeling alive:
globe.ping({ lat, lon, label: "Priya just signed up in Ahmedabad", burst: 18 });
burst adds a particle spray: save it for genuinely notable events, not every trial start.
When you have no live feed
Most sites do not have a websocket to spare for a marketing page. pingFeed replays a list on a
timer, which is fine as long as the events are real:
// Server-rendered from the last 24 hours of real signups, city-level only.
const feed = globe.pingFeed(recentSignups, { interval: 2400, loop: true });
// Later
feed.stop();
The honest version of this widget is "recent signups, on a loop". The dishonest version is a random name generator, and it is dishonest whether or not anyone catches it. If your last real signup was in March, this is not the pattern for you: use Where our customers are instead.
Privacy
A signup ping is personal data wearing a coat.
- Never plot a precise coordinate from an IP address. Round to the city, or use the country
centroid.
countryPoint("DE")gives you a defensible one. - First names only, or no names. "Someone in Berlin just signed up" converts nearly as well and cannot embarrass a customer.
- Ask, or aggregate. If you would not put the row in a public spreadsheet, do not put it on a spinning globe on your home page.
import { countryPoint } from "canvas-globe";
const place = countryPoint(event.countryCode);
if (place) globe.ping({ ...place, label: `Someone in ${event.country} just signed up` });
Pacing
Too fast reads as noise, too slow reads as broken. Rules of thumb:
- One ping every 2-3 seconds is the sweet spot for a hero. Below ~1.5 s it stops registering as individual events.
- If your real rate is higher, sample it. Nobody needs to see all 40 signups a minute.
- If it is lower, do not pad it. Show a counter instead, and let
pingFeedidle. - Pause when the tab is hidden: the library already stops rendering, but stop your feed too so it does not queue up a burst on return.
let feed = globe.pingFeed(recentSignups, { interval: 2400 });
document.addEventListener("visibilitychange", () => {
if (document.hidden) feed.stop();
else feed = globe.pingFeed(recentSignups, { interval: 2400 });
});
The counter is doing half the work
counter eases between values instead of snapping, which is why a number that ticks up feels like
momentum rather than a static stat:
setInterval(async () => {
const { total } = await fetch("/api/signups/total").then((r) => r.json());
globe.setOptions({ counter: { value: total, label: "signups all time" } });
}, 30000);
Poll on a slow interval. The animation covers the gap, and nobody is watching closely enough to notice a 30-second lag on a vanity metric.
Next
- Live pings: the full
pingandpingFeedAPI - Landing page hero globe: this, plus arcs and a scene
- Waitlist milestone: what to do when the number lands