What actually happens
When a site asks for your location, the browser shows a small prompt: "this site wants to know your location. Allow or Block." That prompt is the whole game, and the radar is built around both answers.
Allow, and the browser hands the page a latitude and longitude. The radar drops a red "You" dot there, measures the real distance to every show, and zooms to fit the ones within 200 miles. The caption under the map says how many there are, so a fan instantly knows their favorite DJ is playing twenty minutes away.
Block, and the browser hands the page nothing at all. This is where most map widgets fail: the code crashes, or the map turns into a blank gray rectangle. The radar does neither. It quietly falls back to the experience it always had: every show on a world map, zoomed to fit, with a calm caption saying location isn't available. Plenty of people tap Block, and they're right to be careful. They still get a map that works.
The demo above asks only when you press the button. The "pretend" buttons show the same logic without sharing anything.
Every answer has a designed outcome
- Allowed, shows nearby
- A red "You" marker, the map fitted around you and every show inside the radius, and an announcement like "2 shows within 200 miles". Try Dubai above.
- Allowed, nothing nearby
- The map centers on the visitor's own neighborhood, so it still feels personal. Try Rochelle Park: Anyma's tour doesn't come near New Jersey.
- Declined or unavailable
- The full world view of every show, plus "Location not available, showing all shows". Nothing breaks and nothing is hidden.
- Asked before
- The first answer is remembered, so a visitor who said no is never nagged with the prompt again.
Real distance on a round planet
"Within 200 miles" has to mean miles a person would actually travel, not a box drawn on a flat map. The radar uses the haversine formula, which measures the great-circle distance between two points on a sphere:
function milesBetween(a, b) {
const R = 3958.8; // Earth's radius in miles
const rad = d => d * Math.PI / 180;
const dLat = rad(b.lat - a.lat), dLng = rad(b.lng - a.lng);
const h = Math.sin(dLat / 2) ** 2 +
Math.cos(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.sin(dLng / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(h));
}
The standard formula, as used in the demo. The production code is in src/ on GitHub.
The data contract
One JSON array, rendered into the page by the server or set at runtime. No fetch, no API keys, no backend:
[
{
"lat": 40.759,
"lng": -73.9845,
"title": "Alan Walker",
"venue": "Marquee New York",
"city": "New York City, NY",
"date": "Jun 6",
"url": "https://example.com/tickets"
}
]
Points without numeric coordinates are silently left off the map. A show that never got geocoded must never become a pin at 0,0 in the middle of the South Atlantic.
Why the default map tiles changed
For years the widget used CARTO's free "Dark Matter" map tiles. Then in 2026 CARTO began requiring API keys for keyless traffic, and maps that relied on them started showing error tiles that read "API key required." A widget whose default map can silently break is a widget nobody can trust.
So the default is now Esri's dark gray canvas, which works today with no account (free with attribution, which the widget supplies), and the tile source is a single setting. When a provider changes its policy, you change one attribute, not your code. The demo above runs on that same Esri default.
Accessible, and race-safe
The caption under the map is a real role="status" live region, and it's visible: sighted visitors and screen-reader users get the same sentence for every outcome. With prefers-reduced-motion on, the map skips its pan-and-zoom animations and simply arrives where it means to be.
The boot is guarded against double-initialization and waits for Leaflet before rendering, announcing clearly if the library never arrives. That's not hypothetical: the production loader once hit a bug where Leaflet was injected twice and destroyed an already-initialized map. Its sibling project, sri-lazy-loader, exists to load Leaflet exactly once, verified with Subresource Integrity.
Use it in your project
<link rel="stylesheet" href="leaflet.css">
<link rel="stylesheet" href="src/radar.css">
<script src="src/radar.js" defer></script>
<div data-radar
data-radar-points='[{"lat":40.759,"lng":-73.9845,"title":"Alan Walker","venue":"Marquee New York","city":"New York City, NY","date":"Jun 6","url":"https://..."}]'>
</div>
| Setting | Default | Attribute |
|---|---|---|
| Nearby radius (miles) | 200 | data-radar-radius |
| Zoom when nothing is nearby | 8 | data-radar-fallback-zoom |
| Pin color | #00d2ff | data-radar-color |
| Map tiles | Esri Dark Gray | data-radar-tiles |
| Tiles' maximum zoom | 16 | data-radar-max-zoom |
From the repo's README. Leaflet 1.9+ is the one peer dependency. Geolocation needs HTTPS (or localhost).
Related projects
Frequently asked questions
What happens if a visitor declines location access?
The map still works: it shows all supplied show pins in a global view and announces that location is unavailable instead of rendering an empty map.
How does next-show-radar decide which shows are nearby?
After location is granted, it calculates distances with the Haversine formula and fits shows within the default 200-mile radius. The radius is configurable.
What happens to shows without coordinates?
Points without numeric latitude and longitude are omitted, so an un-geocoded show cannot accidentally appear at 0,0.
Does the widget repeatedly ask for location?
No. The README describes caching the first geolocation result so a visitor is not prompted again after they have answered.
Want features that respect your users?
Bryan Hamilton is a full-stack web engineer with 25+ years in PHP and JavaScript, available immediately for remote, hybrid or on-site roles in the NYC metro area.