Skip to main content

Store and office locator

A locator is the highest-intent map on any site: somebody is trying to come to you. The usual build pulls in a tiles-and-API-key mapping service, a consent banner and 400 KB of JavaScript for what is, at country scale, seven dots and a list.

Loading globe…

The code

import { createGlobe } from "canvas-globe";

const branches = [
{ label: "Ahmedabad", lat: 23.03, lon: 72.58, slug: "ahmedabad", hours: "9-7 Mon-Sat" },
{ label: "Mumbai", lat: 19.08, lon: 72.88, slug: "mumbai", hours: "10-8 daily" },
// …
];

const globe = createGlobe(canvas, {
mode: "map",
preset: "atlas",
projection: "mercator",
autoRotate: false,

center: { lon: 78, lat: 22 },
zoom: 3.4,
zoomable: true,
minZoom: 1,
maxZoom: 8,

markers: branches,
labels: "markers",

tooltip: (m) => `${m.label}: ${m.hours}`,
onClick: (m) => m && selectBranch(m.slug),
onHover: (m) => (canvas.style.cursor = m ? "pointer" : "grab"),
});

// Start where the visitor is. `locateViewer` only resolves the place: // moving the camera is yours to decide.
const place = globe.locateViewer();
if (place) globe.flyTo(place.lon, place.lat, { zoom: 4 });

Mercator, for once

Everywhere else in these docs Mercator gets a warning. Here it is the right answer: at city and regional zoom it preserves local angles and matches the maps most visitors already use. For a locator, that familiarity is more useful than area accuracy over a 500 km view.

Switch to naturalEarth if the locator spans continents.

Starting where they are

locateViewer() resolves the visitor's country and region from their IANA time zone. No permission prompt, no network request, no consent banner entry, and it always returns something, which is more than the Geolocation API can promise.

It is accurate to a country, not a street. That is exactly right for choosing an initial view and completely wrong for "your nearest branch is 2.1 km away". For that, ask:

const place = await globe.locateViewer({ precise: true }); // prompts for GPS
if (place) {
const nearest = nearestBranch(place, branches);
globe.flyTo(nearest.lon, nearest.lat, { zoom: 6 });
highlight(nearest);
}

If the visitor declines, the time-zone estimate is kept. Nothing breaks. See Viewer location.

:::caution Never claim precision you do not have A time-zone estimate can be 1,000 km out. The library draws a dashed accuracy circle for exactly this reason: do not remove it and then label the pin "You are here". :::

The list is the real interface

On a phone, nobody finds a branch by pinching a map. They scroll a list. Build the list first and let the map follow it:

<ul>
{branches.map((b) => (
<li key={b.slug}>
<button
onMouseEnter={() => globe.flyTo(b.lon, b.lat, { zoom: 5 })}
onClick={() => (window.location.href = `/stores/${b.slug}`)}
>
<strong>{b.label}</strong>
<span>{b.address}</span>
<span>{b.hours}</span>
</button>
</li>
))}
</ul>

This gets you three things a canvas alone cannot: crawlable addresses, working browser search, and a locator that still functions if the canvas never paints.

Mark the list up with LocalBusiness structured data while you are there: local SEO cares about the markup, not the map.

Clusters when there are hundreds

For a dealer network or a franchise, turn clustering on and let it collapse by zoom:

{ cluster: true, clusterRadius: 42 }

Markers within clusterRadius screen pixels merge into a counted bubble that splits as you zoom in. Because clustering happens in screen space, density adapts automatically rather than needing a per-zoom configuration. See Clustering.

What this is not

Be clear with yourself about the trade:

NeedThis libraryA tile map
Country and regional view
Street-level detail
Turn-by-turn directions
Address search❌ (bring your own)
Works offline, no key, no consent banner

If someone needs to see which side of the road the door is on, link out to a mapping service for that one branch. Use this for the overview, which is where 90% of the interactions happen anyway.

<a href={`https://www.google.com/maps/search/?api=1&query=${lat},${lon}`}>Directions</a>

Next