Store Locator Patterns Skill Comprehensive patterns for building store locators, restaurant finders, and location-based search applications with Mapbox GL JS. Covers marker display, filtering, distance calculation, interactive lists, and directions integration. When to Use This Skill Use this skill when building applications that: Display multiple locations on a map (stores, restaurants, offices, etc.) Allow users to filter or search locations Calculate distances from user location Provide interactive lists synced with map markers Show location details in popups or side panels Integrate directions to selected locations Dependencies Required: Mapbox GL JS v3.x @turf/turf - For spatial calculations (distance, area, etc.) Installation: npm install mapbox-gl @turf/turf Core Architecture Pattern Overview A typical store locator consists of: Map Display - Shows all locations as markers Location Data - GeoJSON with store/location information Interactive List - Side panel listing all locations Filtering - Search, category filters, distance filters Detail View - Popup or panel with location details User Location - Geolocation for distance calculation Directions - Route to selected location (optional) Data Structure GeoJSON format for locations: { "type" : "FeatureCollection" , "features" : [ { "type" : "Feature" , "geometry" : { "type" : "Point" , "coordinates" : [ -77.034084 , 38.909671 ] } , "properties" : { "id" : "store-001" , "name" : "Downtown Store" , "address" : "123 Main St, Washington, DC 20001" , "phone" : "(202) 555-0123" , "hours" : "Mon-Sat: 9am-9pm, Sun: 10am-6pm" , "category" : "retail" , "website" : "https://example.com/downtown" } } ] } Key properties: id - Unique identifier for each location name - Display name address - Full address for display and geocoding coordinates - [longitude, latitude] format category - For filtering (retail, restaurant, office, etc.) Custom properties as needed (hours, phone, website, etc.) Basic Store Locator Implementation Step 1: Initialize Map and Data import mapboxgl from 'mapbox-gl' ; import 'mapbox-gl/dist/mapbox-gl.css' ; mapboxgl . accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN' ; // Store locations data const stores = { type : 'FeatureCollection' , features : [ { type : 'Feature' , geometry : { type : 'Point' , coordinates : [ - 77.034084 , 38.909671 ] } , properties : { id : 'store-001' , name : 'Downtown Store' , address : '123 Main St, Washington, DC 20001' , phone : '(202) 555-0123' , category : 'retail' } } // ... more stores ] } ; const map = new mapboxgl . Map ( { container : 'map' , style : 'mapbox://styles/mapbox/standard' , center : [ - 77.034084 , 38.909671 ] , zoom : 11 } ) ; Step 2: Add Markers to Map Option 1: HTML Markers (< 100 locations) const markers = { } ; stores . features . forEach ( ( store ) => { // Create marker element const el = document . createElement ( 'div' ) ; el . className = 'marker' ; el . style . backgroundImage = 'url(/marker-icon.png)' ; el . style . width = '30px' ; el . style . height = '40px' ; el . style . backgroundSize = 'cover' ; el . style . cursor = 'pointer' ; // Create marker const marker = new mapboxgl . Marker ( el ) . setLngLat ( store . geometry . coordinates ) . setPopup ( new mapboxgl . Popup ( { offset : 25 } ) . setHTML ( `
${ store . properties . name }
${ store . properties . address }
${ store . properties . phone }
)
)
.
addTo
(
map
)
;
// Store reference for later access
markers
[
store
.
properties
.
id
]
=
marker
;
// Handle marker click
el
.
addEventListener
(
'click'
,
(
)
=>
{
flyToStore
(
store
)
;
createPopup
(
store
)
;
highlightListing
(
store
.
properties
.
id
)
;
}
)
;
}
)
;
Option 2: Symbol Layer (100-1000 locations)
map
.
on
(
'load'
,
(
)
=>
{
// Add store data as source
map
.
addSource
(
'stores'
,
{
type
:
'geojson'
,
data
:
stores
}
)
;
// Add custom marker image
map
.
loadImage
(
'/marker-icon.png'
,
(
error
,
image
)
=>
{
if
(
error
)
throw
error
;
map
.
addImage
(
'custom-marker'
,
image
)
;
// Add symbol layer
map
.
addLayer
(
{
id
:
'stores-layer'
,
type
:
'symbol'
,
source
:
'stores'
,
layout
:
{
'icon-image'
:
'custom-marker'
,
'icon-size'
:
0.8
,
'icon-allow-overlap'
:
true
,
'text-field'
:
[
'get'
,
'name'
]
,
'text-font'
:
[
'Open Sans Bold'
,
'Arial Unicode MS Bold'
]
,
'text-offset'
:
[
0
,
1.5
]
,
'text-anchor'
:
'top'
,
'text-size'
:
12
}
}
)
;
}
)
;
// Handle marker clicks using Interactions API (recommended)
map
.
addInteraction
(
'store-click'
,
{
type
:
'click'
,
target
:
{
layerId
:
'stores-layer'
}
,
handler
:
(
e
)
=>
{
const
store
=
e
.
feature
;
flyToStore
(
store
)
;
createPopup
(
store
)
;
}
}
)
;
// Or using traditional event listener:
// map.on('click', 'stores-layer', (e) => {
// const store = e.features[0];
// flyToStore(store);
// createPopup(store);
// });
// Change cursor on hover
map
.
on
(
'mouseenter'
,
'stores-layer'
,
(
)
=>
{
map
.
getCanvas
(
)
.
style
.
cursor
=
'pointer'
;
}
)
;
map
.
on
(
'mouseleave'
,
'stores-layer'
,
(
)
=>
{
map
.
getCanvas
(
)
.
style
.
cursor
=
''
;
}
)
;
}
)
;
Option 3: Clustering (> 1000 locations)
map
.
on
(
'load'
,
(
)
=>
{
map
.
addSource
(
'stores'
,
{
type
:
'geojson'
,
data
:
stores
,
cluster
:
true
,
clusterMaxZoom
:
14
,
clusterRadius
:
50
}
)
;
// Cluster circles
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
]
}
}
)
;
// Cluster count labels
map
.
addLayer
(
{
id
:
'cluster-count'
,
type
:
'symbol'
,
source
:
'stores'
,
filter
:
[
'has'
,
'point_count'
]
,
layout
:
{
'text-field'
:
'{point_count_abbreviated}'
,
'text-font'
:
[
'DIN Offc Pro Medium'
,
'Arial Unicode MS Bold'
]
,
'text-size'
:
12
}
}
)
;
// Unclustered points
map
.
addLayer
(
{
id
:
'unclustered-point'
,
type
:
'circle'
,
source
:
'stores'
,
filter
:
[
'!'
,
[
'has'
,
'point_count'
]
]
,
paint
:
{
'circle-color'
:
'#11b4da'
,
'circle-radius'
:
8
,
'circle-stroke-width'
:
1
,
'circle-stroke-color'
:
'#fff'
}
}
)
;
// Zoom on cluster click
map
.
on
(
'click'
,
'clusters'
,
(
e
)
=>
{
const
features
=
map
.
queryRenderedFeatures
(
e
.
point
,
{
layers
:
[
'clusters'
]
}
)
;
const
clusterId
=
features
[
0
]
.
properties
.
cluster_id
;
map
.
getSource
(
'stores'
)
.
getClusterExpansionZoom
(
clusterId
,
(
err
,
zoom
)
=>
{
if
(
err
)
return
;
map
.
easeTo
(
{
center
:
features
[
0
]
.
geometry
.
coordinates
,
zoom
:
zoom
}
)
;
}
)
;
}
)
;
// Show popup on unclustered point click
map
.
on
(
'click'
,
'unclustered-point'
,
(
e
)
=>
{
const
coordinates
=
e
.
features
[
0
]
.
geometry
.
coordinates
.
slice
(
)
;
const
props
=
e
.
features
[
0
]
.
properties
;
new
mapboxgl
.
Popup
(
)
.
setLngLat
(
coordinates
)
.
setHTML
(
${ props . name }
${ props . address }
)
.
addTo
(
map
)
;
}
)
;
}
)
;
Step 3: Build Interactive Location List
function
buildLocationList
(
stores
)
{
const
listingContainer
=
document
.
getElementById
(
'listings'
)
;
stores
.
features
.
forEach
(
(
store
,
index
)
=>
{
const
listing
=
listingContainer
.
appendChild
(
document
.
createElement
(
'div'
)
)
;
listing
.
id
=
listing-
${
store
.
properties
.
id
}
;
listing
.
className
=
'listing'
;
const
link
=
listing
.
appendChild
(
document
.
createElement
(
'a'
)
)
;
link
.
href
=
'#'
;
link
.
className
=
'title'
;
link
.
id
=
link-
${
store
.
properties
.
id
}
;
link
.
innerHTML
=
store
.
properties
.
name
;
const
details
=
listing
.
appendChild
(
document
.
createElement
(
'div'
)
)
;
details
.
innerHTML
=
<
p
${ store . properties . address } </ p
< p
${ store . properties . phone || '' } </ p
; // Handle listing click link . addEventListener ( 'click' , ( e ) => { e . preventDefault ( ) ; flyToStore ( store ) ; createPopup ( store ) ; highlightListing ( store . properties . id ) ; } ) ; } ) ; } function flyToStore ( store ) { map . flyTo ( { center : store . geometry . coordinates , zoom : 15 , duration : 1000 } ) ; } function createPopup ( store ) { const popups = document . getElementsByClassName ( 'mapboxgl-popup' ) ; // Remove existing popups if ( popups [ 0 ] ) popups [ 0 ] . remove ( ) ; new mapboxgl . Popup ( { closeOnClick : true } ) . setLngLat ( store . geometry . coordinates ) . setHTML (
${ store . properties . name }
${ store . properties . address }
${ store . properties . phone }
${
store
.
properties
.
website
?
<a href="
${
store
.
properties
.
website
}
" target="_blank">Visit Website</a>
:
''
}
)
.
addTo
(
map
)
;
}
function
highlightListing
(
id
)
{
// Remove existing highlights
const
activeItem
=
document
.
getElementsByClassName
(
'active'
)
;
if
(
activeItem
[
0
]
)
{
activeItem
[
0
]
.
classList
.
remove
(
'active'
)
;
}
// Add highlight to selected listing
const
listing
=
document
.
getElementById
(
listing-
${
id
}
`
)
;
listing
.
classList
.
add
(
'active'
)
;
// Scroll to listing
listing
.
scrollIntoView
(
{
behavior
:
'smooth'
,
block
:
'nearest'
}
)
;
}
// Build the list on load
map
.
on
(
'load'
,
(
)
=>
{
buildLocationList
(
stores
)
;
}
)
;
Step 4: Add Search/Filter Functionality
Text Search:
function
filterStores
(
searchTerm
)
{
const
filtered
=
{
type
:
'FeatureCollection'
,
features
:
stores
.
features
.
filter
(
(
store
)
=>
{
const
name
=
store
.
properties
.
name
.
toLowerCase
(
)
;
const
address
=
store
.
properties
.
address
.
toLowerCase
(
)
;
const
search
=
searchTerm
.
toLowerCase
(
)
;
return
name
.
includes
(
search
)
||
address
.
includes
(
search
)
;
}
)
}
;
// Update map source
if
(
map
.
getSource
(
'stores'
)
)
{
map
.
getSource
(
'stores'
)
.
setData
(
filtered
)
;
}
// Rebuild listing
document
.
getElementById
(
'listings'
)
.
innerHTML
=
''
;
buildLocationList
(
filtered
)
;
// Fit map to filtered results
if
(
filtered
.
features
.
length
0 ) { const bounds = new mapboxgl . LngLatBounds ( ) ; filtered . features . forEach ( ( feature ) => { bounds . extend ( feature . geometry . coordinates ) ; } ) ; map . fitBounds ( bounds , { padding : 50 } ) ; } } // Add search input handler document . getElementById ( 'search-input' ) . addEventListener ( 'input' , ( e ) => { filterStores ( e . target . value ) ; } ) ; Category Filter: function filterByCategory ( category ) { const filtered = category === 'all' ? stores : { type : 'FeatureCollection' , features : stores . features . filter ( ( store ) => store . properties . category === category ) } ; // Update map and list if ( map . getSource ( 'stores' ) ) { map . getSource ( 'stores' ) . setData ( filtered ) ; } document . getElementById ( 'listings' ) . innerHTML = '' ; buildLocationList ( filtered ) ; } // Category dropdown document . getElementById ( 'category-select' ) . addEventListener ( 'change' , ( e ) => { filterByCategory ( e . target . value ) ; } ) ; Step 5: Add Geolocation and Distance Calculation let userLocation = null ; // Add geolocation control map . addControl ( new mapboxgl . GeolocateControl ( { positionOptions : { enableHighAccuracy : true } , trackUserLocation : true , showUserHeading : true } ) ) ; // Get user location navigator . geolocation . getCurrentPosition ( ( position ) => { userLocation = [ position . coords . longitude , position . coords . latitude ] ; // Calculate distances and sort const storesWithDistance = stores . features . map ( ( store ) => { const distance = calculateDistance ( userLocation , store . geometry . coordinates ) ; return { ... store , properties : { ... store . properties , distance : distance } } ; } ) ; // Sort by distance storesWithDistance . sort ( ( a , b ) => a . properties . distance - b . properties . distance ) ; // Update data stores . features = storesWithDistance ; // Rebuild list with distances document . getElementById ( 'listings' ) . innerHTML = '' ; buildLocationList ( stores ) ; } , ( error ) => { console . error ( 'Error getting location:' , error ) ; } ) ; // Calculate distance using Turf.js (recommended) import * as turf from '@turf/turf' ; 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 ) ; // Distance in miles } // Update listing to show distance function buildLocationList ( stores ) { const listingContainer = document . getElementById ( 'listings' ) ; stores . features . forEach ( ( store ) => { const listing = listingContainer . appendChild ( document . createElement ( 'div' ) ) ; listing . id =
listing- ${ store . properties . id }; listing . className = 'listing' ; const link = listing . appendChild ( document . createElement ( 'a' ) ) ; link . href = '#' ; link . className = 'title' ; link . innerHTML = store . properties . name ; const details = listing . appendChild ( document . createElement ( 'div' ) ) ; details . innerHTML =${ store . properties . distance ?
${ store . properties . distance } mi
` : '' } < p
${ store . properties . address } </ p
< p
${ store . properties . phone || '' } </ p
; link . addEventListener ( 'click' , ( e ) => { e . preventDefault ( ) ; flyToStore ( store ) ; createPopup ( store ) ; highlightListing ( store . properties . id ) ; } ) ; } ) ; } Step 6: Integrate Directions (Optional) 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 ] } ?+steps=true&geometries=geojson&access_token= ${ mapboxgl . accessToken }) ; const data = await query . json ( ) ; const route = data . routes [ 0 ] ; // Display route on map if ( map . getSource ( 'route' ) ) { map . getSource ( 'route' ) . setData ( { type : 'Feature' , geometry : route . geometry } ) ; } else { map . addSource ( 'route' , { type : 'geojson' , data : { type : 'Feature' , geometry : route . geometry } } ) ; map . addLayer ( { id : 'route' , type : 'line' , source : 'route' , paint : { 'line-color' : '#3b9ddd' , 'line-width' : 5 , 'line-opacity' : 0.75 } } ) ; } // Display directions info const duration = Math . floor ( route . duration / 60 ) ; const distance = ( route . distance * 0.000621371 ) . toFixed ( 1 ) ; // Convert to miles return { duration , distance , steps : route . legs [ 0 ] . steps } ; } // Add "Get Directions" button to popup function createPopup ( store ) { const popups = document . getElementsByClassName ( 'mapboxgl-popup' ) ; if ( popups [ 0 ] ) popups [ 0 ] . remove ( ) ; const popup = new mapboxgl . Popup ( { closeOnClick : true } ) . setLngLat ( store . geometry . coordinates ) . setHTML (
${ store . properties . name }
${ store . properties . address }
${ store . properties . phone }
${
userLocation
?
''
:
''
}
)
.
addTo
(
map
)
;
// Handle directions button
if
(
userLocation
)
{
document
.
getElementById
(
'get-directions'
)
.
addEventListener
(
'click'
,
async
(
)
=>
{
const
directions
=
await
getDirections
(
userLocation
,
store
.
geometry
.
coordinates
)
;
// Update popup with directions
popup
.
setHTML
(
${ store . properties . name }
${ directions . distance } mi • ${ directions . duration } min
${ store . properties . address }
${ step . maneuver . instruction }
` ) . join ( '' ) }` ) ; } ) ; } } Styling Patterns Layout Structure <! DOCTYPE html
< html
< head
< meta charset = " utf-8 " /> < title
Store Locator </ title
< meta name = " viewport " content = " width=device-width, initial-scale=1 " /> < link href = " https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.css " rel = " stylesheet " /> < style
body { margin : 0 ; padding : 0 ; font-family : 'Arial' , sans-serif ; }
app
{ display : flex ; height : 100 vh ; } / Sidebar / .sidebar { width : 400 px ; height : 100 vh ; overflow-y : scroll ; background-color :
fff
; border-right : 1 px solid
ddd
; } .sidebar-header { padding : 20 px ; background-color :
f8f9fa
; border-bottom : 1 px solid
ddd
; } .sidebar-header h1 { margin : 0 0 10 px 0 ; font-size : 24 px ; } / Search / .search-box { width : 100 % ; padding : 10 px ; border : 1 px solid
ddd
; border-radius : 4 px ; font-size : 14 px ; box-sizing : border-box ; } .filter-group { margin-top : 10 px ; } .filter-group select { width : 100 % ; padding : 8 px ; border : 1 px solid
ddd
; border-radius : 4 px ; font-size : 14 px ; } / Listings /
listings
{ padding : 0 ; } .listing { padding : 15 px 20 px ; border-bottom : 1 px solid
eee
; cursor : pointer ; transition : background-color 0.2 s ; } .listing :hover { background-color :
f8f9fa
; } .listing .active { background-color :
e3f2fd
; border-left : 3 px solid
2196f3
; } .listing .title { display : block ; color :
333
; font-weight : bold ; font-size : 16 px ; text-decoration : none ; margin-bottom : 5 px ; } .listing .title :hover { color :
2196f3
; } .listing p { margin : 5 px 0 ; font-size : 14 px ; color :
666
; } .listing .distance { color :
2196f3
; font-weight : bold ; } / Map /
map
{ flex : 1 ; height : 100 vh ; } / Popups / .mapboxgl-popup-content { padding : 15 px ; font-family : 'Arial' , sans-serif ; } .mapboxgl-popup-content h3 { margin : 0 0 10 px 0 ; font-size : 18 px ; } .mapboxgl-popup-content p { margin : 5 px 0 ; font-size : 14 px ; } .mapboxgl-popup-content button { margin-top : 10 px ; padding : 8 px 16 px ; background-color :
2196f3
; color : white ; border : none ; border-radius : 4 px ; cursor : pointer ; font-size : 14 px ; } .mapboxgl-popup-content button :hover { background-color :
1976d2
; } / Responsive / @media ( max-width : 768 px ) {
app
{ flex-direction : column ; } .sidebar { width : 100 % ; height : 50 vh ; }
map
{ height : 50 vh ; } } </ style
</ head
< body
< div id = " app "
< div class = " sidebar "
< div class = " sidebar-header "
< h1
Store Locator </ h1
< input type = " text " id = " search-input " class = " search-box " placeholder = " Search by name or address... " /> < div class = " filter-group "
< select id = " category-select "
< option value = " all "
All Categories </ option
< option value = " retail "
Retail </ option
< option value = " restaurant "
Restaurant </ option
< option value = " office "
Office </ option
</ select
</ div
</ div
< div id = " listings "
</ div
</ div
< div id = " map "
</ div
</ div
< script src = " https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.js "
</ script
< script src = " app.js "
</ script
</ body
</ html
Custom Marker Styling / Custom marker styles / .marker { background-size : cover ; width : 30 px ; height : 40 px ; cursor : pointer ; transition : transform 0.2 s ; } .marker :hover { transform : scale ( 1.1 ) ; } / Category-specific marker colors / .marker .retail { background-color :
2196f3
; } .marker .restaurant { background-color :
f44336
; } .marker .office { background-color :
4caf50
;
}
Performance Optimization
Debounced Search
function
debounce
(
func
,
wait
)
{
let
timeout
;
return
function
executedFunction
(
...
args
)
{
const
later
=
(
)
=>
{
clearTimeout
(
timeout
)
;
func
(
...
args
)
;
}
;
clearTimeout
(
timeout
)
;
timeout
=
setTimeout
(
later
,
wait
)
;
}
;
}
const
debouncedFilter
=
debounce
(
filterStores
,
300
)
;
document
.
getElementById
(
'search-input'
)
.
addEventListener
(
'input'
,
(
e
)
=>
{
debouncedFilter
(
e
.
target
.
value
)
;
}
)
;
Best Practices
Data Management
// ✅ GOOD: Load data once, filter in memory
const
allStores
=
await
fetch
(
'/api/stores'
)
.
then
(
(
r
)
=>
r
.
json
(
)
)
;
function
filterStores
(
criteria
)
{
return
{
type
:
'FeatureCollection'
,
features
:
allStores
.
features
.
filter
(
criteria
)
}
;
}
// ❌ BAD: Fetch on every filter
async
function
filterStores
(
criteria
)
{
return
await
fetch
(
/api/stores?filter=
${
criteria
}
)
.
then
(
(
r
)
=>
r
.
json
(
)
)
;
}
Error Handling
// Geolocation error handling
navigator
.
geolocation
.
getCurrentPosition
(
successCallback
,
(
error
)
=>
{
let
message
=
'Unable to get your location.'
;
switch
(
error
.
code
)
{
case
error
.
PERMISSION_DENIED
:
message
=
'Please enable location access to see nearby stores.'
;
break
;
case
error
.
POSITION_UNAVAILABLE
:
message
=
'Location information is unavailable.'
;
break
;
case
error
.
TIMEOUT
:
message
=
'Location request timed out.'
;
break
;
}
showNotification
(
message
)
;
}
,
{
enableHighAccuracy
:
true
,
timeout
:
5000
,
maximumAge
:
0
}
)
;
// API error handling
async
function
loadStores
(
)
{
try
{
const
response
=
await
fetch
(
'/api/stores'
)
;
if
(
!
response
.
ok
)
{
throw
new
Error
(
HTTP error! status:
${
response
.
status
}
)
;
}
const
data
=
await
response
.
json
(
)
;
return
data
;
}
catch
(
error
)
{
console
.
error
(
'Failed to load stores:'
,
error
)
;
showNotification
(
'Unable to load store locations. Please try again.'
)
;
return
{
type
:
'FeatureCollection'
,
features
:
[
]
}
;
}
}
Accessibility
// Add ARIA labels
document
.
getElementById
(
'search-input'
)
.
setAttribute
(
'aria-label'
,
'Search stores'
)
;
// Keyboard navigation
document
.
querySelectorAll
(
'.listing'
)
.
forEach
(
(
listing
,
index
)
=>
{
listing
.
setAttribute
(
'tabindex'
,
'0'
)
;
listing
.
setAttribute
(
'role'
,
'button'
)
;
listing
.
setAttribute
(
'aria-label'
,
View
${
listing
.
querySelector
(
'.title'
)
.
textContent
}
)
;
listing
.
addEventListener
(
'keypress'
,
(
e
)
=>
{
if
(
e
.
key
===
'Enter'
||
e
.
key
===
' '
)
{
e
.
preventDefault
(
)
;
listing
.
click
(
)
;
}
}
)
;
}
)
;
// Focus management
function
highlightListing
(
id
)
{
const
listing
=
document
.
getElementById
(
listing-
${
id
}
)
;
listing
.
classList
.
add
(
'active'
)
;
listing
.
focus
(
)
;
listing
.
scrollIntoView
(
{
behavior
:
'smooth'
,
block
:
'nearest'
}
)
;
}
Common Variations
Mobile-First Layout
/ Mobile first: stack sidebar on top /
@media
(
max-width
:
768
px
)
{
app
{ flex-direction : column ; } .sidebar { width : 100 % ; height : 40 vh ; max-height : 40 vh ; }
map
{ height : 60 vh ; } / Toggle sidebar / .sidebar .collapsed { height : 60 px ; } } Fullscreen Map with Overlay // Map takes full screen, list appears as overlay const listOverlay = document . createElement ( 'div' ) ; listOverlay . className = 'list-overlay' ; listOverlay . innerHTML = ` < button id = " toggle-list "
View All Locations ( ${ stores . features . length } ) </ button
< div id = " listings " class = " hidden "
</ div
; document . getElementById ( 'toggle-list' ) . addEventListener ( 'click' , ( ) => { document . getElementById ( 'listings' ) . classList . toggle ( 'hidden' ) ; } ) ; Map-Only View // No sidebar, everything in popups function createDetailedPopup ( store ) { const popup = new mapboxgl . Popup ( { maxWidth : '400px' } ) . setLngLat ( store . geometry . coordinates ) . setHTML (
${ store . properties . name }
${ store . properties . address }
${ store . properties . phone }
${ store . properties . hours }
${ store . properties . distance ? `${ store . properties . distance } mi away
` : '' }` ) . addTo ( map ) ; } Framework Integration React Implementation import { useEffect , useRef , useState } from 'react' ; import mapboxgl from 'mapbox-gl' ; function StoreLocator ( { stores } ) { const mapContainer = useRef ( null ) ; const map = useRef ( null ) ; const [ selectedStore , setSelectedStore ] = useState ( null ) ; const [ filteredStores , setFilteredStores ] = useState ( stores ) ; useEffect ( ( ) => { if ( map . current ) return ; map . current = new mapboxgl . Map ( { container : mapContainer . current , style : 'mapbox://styles/mapbox/standard' , center : [ - 77.034084 , 38.909671 ] , zoom : 11 } ) ; map . current . on ( 'load' , ( ) => { map . current . addSource ( 'stores' , { type : 'geojson' , data : filteredStores } ) ; map . current . addLayer ( { id : 'stores' , type : 'circle' , source : 'stores' , paint : { 'circle-color' : '#2196f3' , 'circle-radius' : 8 } } ) ; map . current . on ( 'click' , 'stores' , ( e ) => { setSelectedStore ( e . features [ 0 ] ) ; } ) ; } ) ; return ( ) => map . current . remove ( ) ; } , [ ] ) ; // Update source when filtered stores change useEffect ( ( ) => { if ( map . current && map . current . getSource ( 'stores' ) ) { map . current . getSource ( 'stores' ) . setData ( filteredStores ) ; } } , [ filteredStores ] ) ; return ( < div className = " store-locator "
< Sidebar stores = { filteredStores } selectedStore = { selectedStore } onStoreClick = { setSelectedStore } onFilter = { setFilteredStores } /> < div ref = { mapContainer } className = " map-container " /> </ div
) ; } Resources Turf.js - Spatial analysis library (recommended for distance calculations) Mapbox GL JS API Interactions API Guide GeoJSON Specification Directions API Store Locator Tutorial