Store Locator Patterns
Quick reference for building store locators and location finders with Mapbox.
Architecture
Core Components:
- Map with markers
- Location data (GeoJSON)
- Interactive list
- Search/filter
- User location + distance
- Directions (optional)
Data Structure
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-77.034084, 38.909671]
},
"properties": {
"id": "store-001",
"name": "Downtown Store",
"address": "123 Main St, DC 20001",
"phone": "(202) 555-0123",
"category": "retail",
"hours": "Mon-Sat: 9am-9pm"
}
}
]
}
Marker Strategies
| Locations | Strategy | Implementation |
|---|---|---|
| < 100 | HTML Markers | new mapboxgl.Marker() |
| 100-1000 | Symbol Layer | addLayer({ type: 'symbol' }) |
| > 1000 | Clustering | cluster: true in source |
HTML Markers Pattern
stores.features.forEach((store) => {
const marker = new mapboxgl.Marker()
.setLngLat(store.geometry.coordinates)
.setPopup(
new mapboxgl.Popup().setHTML(`
<h3>${store.properties.name}</h3>
<p>${store.properties.address}</p>
`)
)
.addTo(map);
});
Symbol Layer Pattern
map.on('load', () => {
map.addSource('stores', {
type: 'geojson',
data: stores
});
map.addLayer({
id: 'stores',
type: 'symbol',
source: 'stores',
layout: {
'icon-image': 'custom-marker',
'icon-size': 0.8,
'text-field': ['get', 'name'],
'text-offset': [0, 1.5]
}
});
// Using Interactions API (recommended)
map.addInteraction('store-click', {
type: 'click',
target: { layerId: 'stores' },
handler: (e) => {
const store = e.feature;
showStoreDetails(store);
}
});
// Or using traditional event listener
// map.on('click', 'stores', (e) => {
// const store = e.features[0];
// showStoreDetails(store);
// });
});
Clustering Pattern
map.addSource('stores', {
type: 'geojson',
data: stores,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
// Clusters
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'stores',
filter: ['has', 'point_count'],
paint: {
'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 10, '#f1f075', 30, '#f28cb1'],
'circle-radius': ['step', ['get', 'point_count'], 20, 10, 30, 30, 40]
}
});
// Unclustered points
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'stores',
filter: ['!', ['has', 'point_count']],
paint: { 'circle-color': '#11b4da', 'circle-radius': 8 }
});
Interactive List
function buildLocationList(stores) {
const container = document.getElementById('listings');
container.innerHTML = '';
stores.features.forEach((store) => {
const listing = document.createElement('div');
listing.className = 'listing';
listing.innerHTML = `
<a href="#" class="title">${store.properties.name}</a>
<p>${store.properties.address}</p>
${store.properties.distance ? `<p class="distance">${store.properties.distance} mi</p>` : ''}
`;
listing.querySelector('.title').addEventListener('click', (e) => {
e.preventDefault();
flyToStore(store);
createPopup(store);
});
container.appendChild(listing);
});
}
function flyToStore(store) {
map.flyTo({
center: store.geometry.coordinates,
zoom: 15
});
}
Search/Filter
Text Search:
function filterStores(query) {
const filtered = {
type: 'FeatureCollection',
features: stores.features.filter((store) => {
const name = store.properties.name.toLowerCase();
const address = store.properties.address.toLowerCase();
return name.includes(query.toLowerCase()) || address.includes(query.toLowerCase());
})
};
map.getSource('stores').setData(filtered);
buildLocationList(filtered);
}
document.getElementById('search').addEventListener('input', (e) => {
filterStores(e.target.value);
});
Category Filter:
function filterByCategory(category) {
const filtered =
category === 'all'
? stores
: {
type: 'FeatureCollection',
features: stores.features.filter((s) => s.properties.category === category)
};
map.getSource('stores').setData(filtered);
buildLocationList(filtered);
}
Distance Calculation
Using Turf.js (recommended):
import * as turf from '@turf/turf';
// Calculate distance between two points
function calculateDistance(from, to) {
const fromPoint = turf.point(from);
const toPoint = turf.point(to);
const distance = turf.distance(fromPoint, toPoint, { units: 'miles' });
return distance.toFixed(1);
}
// Get user location
navigator.geolocation.getCurrentPosition((position) => {
const userLocation = [position.coords.longitude, position.coords.latitude];
// Add distances
stores.features = stores.features.map((store) => ({
...store,
properties: {
...store.properties,
distance: calculateDistance(userLocation, store.geometry.coordinates)
}
}));
// Sort by distance
stores.features.sort((a, b) => a.properties.distance - b.properties.distance);
buildLocationList(stores);
});
Directions Integration
async function getDirections(from, to) {
const query = await fetch(
`https://api.mapbox.com/directions/v5/mapbox/driving/` +
`${from[0]},${from[1]};${to[0]},${to[1]}?` +
`geometries=geojson&access_token=${mapboxgl.accessToken}`
);
const route = (await query.json()).routes[0];
// Display route
map.getSource('route').setData({
type: 'Feature',
geometry: route.geometry
});
// Add route layer if not exists
if (!map.getLayer('route')) {
map.addLayer({
id: 'route',
type: 'line',
source: 'route',
paint: {
'line-color': '#3b9ddd',
'line-width': 5
}
});
}
return {
duration: Math.floor(route.duration / 60),
distance: (route.distance * 0.000621371).toFixed(1)
};
}
Layout Patterns
Sidebar + Map:
<div style="display: flex; height: 100vh;">
<div class="sidebar" style="width: 400px; overflow-y: scroll;">
<input type="text" id="search" placeholder="Search..." />
<div id="listings"></div>
</div>
<div id="map" style="flex: 1;"></div>
</div>
Mobile Responsive:
@media (max-width: 768px) {
#app {
flex-direction: column;
}
.sidebar {
width: 100%;
height: 40vh;
}
#map {
height: 60vh;
}
}
Performance Tips
// Debounce search
function debounce(func, wait) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
const debouncedSearch = debounce(filterStores, 300);
Geolocation Control
map.addControl(
new mapboxgl.GeolocateControl({
positionOptions: { enableHighAccuracy: true },
trackUserLocation: true,
showUserHeading: true
})
);
Common Patterns
Restaurant Finder:
- Category filters (cuisine type)
- Price range filters
- Rating display
- Hours of operation
- Delivery/pickup options
Office Locator:
- Department filters
- Floor/building numbers
- Contact information
- Meeting room availability
Retail Store Finder:
- Inventory availability
- Store hours
- Services offered
- Appointment booking
Quick Decisions
Need clustering? → Yes if > 1000 locations
Need search? → Always include for > 10 locations
Need directions? → Yes for physical locations users visit
Need distance sorting? → Yes if user location available
Need filters? → Yes if > 20 locations or multiple categories
Resources
- Turf.js - Spatial analysis (distance, area, etc.)
- Interactions API - Modern event handling
- Store Locator Tutorial
- Directions API