Skip to main content

Build a store locator using Mapbox GL JS

Prerequisite

Familiarity with front-end development concepts. Some advanced JavaScript required.

Tutorial series: Store locator

This series of tutorials teaches you how to create a store locator using Mapbox GL JS then sort the stores by their distance from a given location using the Mapbox GL Geocoder plugin and Turf.js:- Part 1: Build a store locator- Part 2: Sort stores by distance

This guide will walk you through how to create a store locator map using Mapbox GL JS. You'll be able to browse all the store locations from a sidebar and click a specific store to view more information. Selecting a marker on the map will highlight the selected store on the sidebar.

You will use Sweetgreen as an example. They have a healthy number of locations, plus their salads are delicious!

This guide shows you how to use Mapbox GL JS to build an interactive web map. If you're new to Mapbox GL JS, you might want to read our guide on Mapbox web applications first.

Getting started

For this project, we recommend that you create a local folder called "store-locator" to house your project files. You'll see this folder referred to as your project folder.

There are a few resources you'll need before getting started:

  • A style URL. A style URL points to a map style you can use with Mapbox. You can create a custom style with the Mapbox Studio style editor or use a Mapbox-owned style.
  • An access token from your Mapbox account. You will use an access token to associate a map with your account. Your access tokens are located on the Access Token page of your Developer Console.
  • Mapbox GL JS. The Mapbox JavaScript library that uses WebGL to render interactive maps from Mapbox styles.
  • A text editor. You'll be writing HTML, CSS, and JavaScript in this tutorial.
  • Data. We collected some of Sweetgreen's locations and marked up the data in GeoJSON. You will add this data to your app later in the tutorial.
  • Custom map marker. You'll be using an image for your map marker. Save the image to your project folder.
arrow-downDownload custom marker

Add structure

In your project folder, create an index.html file. Set up the HTML document by adding Mapbox GL JS inside the <head> of the file:

<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css"
rel="stylesheet"
/>

Next, in the <body> of the file, create a map container and a sidebar that will contain the store listings:

<div class="sidebar">
<div class="heading">
<h1>Our locations</h1>
</div>
<div id="listings" class="listings"></div>
</div>
<div id="map" class="map"></div>

Then, add some CSS in the <head> of the file to create the page layout:

<style>
* {
box-sizing: border-box;
}

body {
color: #404040;
font:
400 15px/22px 'Source Sans Pro',
'Helvetica Neue',
sans-serif;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
}

h1 {
font-size: 22px;
margin: 0;
font-weight: 400;
line-height: 20px;
padding: 20px 2px;
}

a {
color: #404040;
text-decoration: none;
}

a:hover {
color: #101010;
}

/* The page is split between map and sidebar - the sidebar gets 1/3, map
gets 2/3 of the page. You can adjust this to your personal liking. */
.sidebar {
position: absolute;
width: 33.3333%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
border-right: 1px solid rgb(0 0 0 / 25%);
}

.map {
position: absolute;
left: 33.3333%;
width: 66.6666%;
top: 0;
bottom: 0;
}

.heading {
background: #fff;
border-bottom: 1px solid #eee;
height: 60px;
line-height: 60px;
padding: 0 10px;
}
</style>

Save your work, and load the page in your browser. You will see a sidebar and an empty div that will hold the map.

Initialize the map

Now that you have the structure of the page, you will initialize the map with Mapbox GL JS.

First, add your access token using mapboxgl.accessToken. Then, create a new map object using new mapboxgl.Map() and store it in a constant called map. Add the following code before your closing </body> tag:

<script>
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN ';

const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v11',
center: [-77.034084, 38.909671],
zoom: 13,
scrollZoom: false
});
</script>

As you can see above, the Mapbox GL JS map requires several options:

  • container: the id of the <div> element on the page where the map should live. In this case, the id for the <div> is 'map'.
  • style: the style URL for the map style. In this case, use the Mapbox Light map which has the style URL mapbox://styles/mapbox/light-11.
  • center: the initial centerpoint of the map in [longitude, latitude] format.
  • zoom: the initial zoom level of the map.

Save your work, then reload the page in your browser. You will see the map loaded on the right side of the page.

Load data

With Mapbox GL JS, map rendering happens in the browser. For the browser to render your map, you need to add a layer with geospatial data and instructions for how that data should be rendered.

To add a source to the map, your code needs to access the geospatial data. Store the GeoJSON with the Sweetgreen location data inside your <script> tag in a constant called stores:

const stores = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.034084142948, 38.909671288923]
},
'properties': {
'phoneFormatted': '(202) 234-7336',
'phone': '2022347336',
'address': '1471 P St NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at 15th St NW',
'postalCode': '20005',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.049766, 38.900772]
},
'properties': {
'phoneFormatted': '(202) 507-8357',
'phone': '2025078357',
'address': '2221 I St NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at 22nd St NW',
'postalCode': '20037',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.043929, 38.910525]
},
'properties': {
'phoneFormatted': '(202) 387-9338',
'phone': '2023879338',
'address': '1512 Connecticut Ave NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at Dupont Circle',
'postalCode': '20036',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.0672, 38.90516896]
},
'properties': {
'phoneFormatted': '(202) 337-9338',
'phone': '2023379338',
'address': '3333 M St NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at 34th St NW',
'postalCode': '20007',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.002583742142, 38.887041080933]
},
'properties': {
'phoneFormatted': '(202) 547-9338',
'phone': '2025479338',
'address': '221 Pennsylvania Ave SE',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'btwn 2nd & 3rd Sts. SE',
'postalCode': '20003',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-76.933492720127, 38.99225245786]
},
'properties': {
'address': '8204 Baltimore Ave',
'city': 'College Park',
'country': 'United States',
'postalCode': '20740',
'state': 'MD'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.097083330154, 38.980979]
},
'properties': {
'phoneFormatted': '(301) 654-7336',
'phone': '3016547336',
'address': '4831 Bethesda Ave',
'cc': 'US',
'city': 'Bethesda',
'country': 'United States',
'postalCode': '20814',
'state': 'MD'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.359425054188, 38.958058116661]
},
'properties': {
'phoneFormatted': '(571) 203-0082',
'phone': '5712030082',
'address': '11935 Democracy Dr',
'city': 'Reston',
'country': 'United States',
'crossStreet': 'btw Explorer & Library',
'postalCode': '20190',
'state': 'VA'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.10853099823, 38.880100922392]
},
'properties': {
'phoneFormatted': '(703) 522-2016',
'phone': '7035222016',
'address': '4075 Wilson Blvd',
'city': 'Arlington',
'country': 'United States',
'crossStreet': 'at N Randolph St.',
'postalCode': '22203',
'state': 'VA'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-75.28784, 40.008008]
},
'properties': {
'phoneFormatted': '(610) 642-9400',
'phone': '6106429400',
'address': '68 Coulter Ave',
'city': 'Ardmore',
'country': 'United States',
'postalCode': '19003',
'state': 'PA'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-75.20121216774, 39.954030175164]
},
'properties': {
'phoneFormatted': '(215) 386-1365',
'phone': '2153861365',
'address': '3925 Walnut St',
'city': 'Philadelphia',
'country': 'United States',
'postalCode': '19104',
'state': 'PA'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.043959498405, 38.903883387232]
},
'properties': {
'phoneFormatted': '(202) 331-3355',
'phone': '2023313355',
'address': '1901 L St. NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at 19th St',
'postalCode': '20036',
'state': 'D.C.'
}
}
]
};

After you define the stores object, assign a unique ID to each store. You'll use this id later to associate each point on the map with a listing in the sidebar.

/* Assign a unique ID to each store */
stores.features.forEach(function (store, i) {
store.properties.id = i;
});

Now you can add a layer that contains this data and describes how it should be rendered. Add the data to your map once the map loads using the Mapbox GL JS addLayer() method. Create a new circle layer, and specify stores as a GeoJSON data source. After your stores constant, add the following code:

map.on('load', () => {
/* Add the data to your map as a layer */
map.addLayer({
id: 'locations',
type: 'circle',
/* Add a GeoJSON source containing place coordinates and information. */
source: {
type: 'geojson',
data: stores
}
});
});

Save your work, and reload the page in your browser. You will see a circle at each store location listed in the GeoJSON.

Build store listing

Now that the points are on your map, it's time to build the restaurant location listing by iterating through the GeoJSON and creating a list of restaurants dynamically. This means that if you need to add a location then you only need to update the GeoJSON.

Create a new function, buildLocationList, to iterate through the Sweetgreen locations and add each one to the sidebar listing:

function buildLocationList(stores) {
for (const store of stores.features) {
/* Add a new listing section to the sidebar. */
const listings = document.getElementById('listings');
const listing = listings.appendChild(document.createElement('div'));
/* Assign a unique `id` to the listing. */
listing.id = `listing-${store.properties.id}`;
/* Assign the `item` class to each listing for styling. */
listing.className = 'item';

/* Add the link to the individual listing created above. */
const link = listing.appendChild(document.createElement('a'));
link.href = '#';
link.className = 'title';
link.id = `link-${store.properties.id}`;
link.innerHTML = `${store.properties.address}`;

/* Add details to the individual listing. */
const details = listing.appendChild(document.createElement('div'));
details.innerHTML = `${store.properties.city}`;
if (store.properties.phone) {
details.innerHTML += ` &middot; ${store.properties.phoneFormatted}`;
}
if (store.properties.distance) {
const roundedDistance = Math.round(store.properties.distance * 100) / 100;
details.innerHTML += `<div><strong>${roundedDistance} miles away</strong></div>`;
}
}
}

Next, inside your map.on('load', () => {}) function, after addLayer(), call this function when the map loads:

buildLocationList(stores);

Next, add the following to your CSS to accommodate the layout changes:

.listings {
height: 100%;
overflow: auto;
padding-bottom: 60px;
}

.listings .item {
border-bottom: 1px solid #eee;
padding: 10px;
text-decoration: none;
}

.listings .item:last-child {
border-bottom: none;
}

.listings .item .title {
display: block;
color: #00853e;
font-weight: 700;
}

.listings .item .title small {
font-weight: 400;
}

.listings .item.active .title,
.listings .item .title:hover {
color: #8cc63f;
}

.listings .item.active {
background-color: #f8f8f8;
}

::-webkit-scrollbar {
width: 3px;
height: 3px;
border-left: 0;
background: rgba(0 0 0 0.1);
}

::-webkit-scrollbar-track {
background: none;
}

::-webkit-scrollbar-thumb {
background: #00853e;
border-radius: 0;
}

Save your work and reload the page in the browser. The store listings will populate the sidebar on the left side of the page.

Make the map interactive

When a user clicks a link in the sidebar or on a point on the map, you want three things to happen:

  1. The map should fly to the associated store location.
  2. A popup should be displayed at that point.
  3. The listing should be highlighted in the sidebar.

This will require more code, but you can do it!

Define interactivity functions

First, define two functions:

  • flyToStore uses the Mapbox GL JS flyTo method to center the map on the correct store location and zoom in.
  • createPopUp displays a Mapbox GL JS Popup at that same store location.

These functions will be fired both when a user clicks on a link in the sidebar listing and when a user clicks on a store location in the map. (Highlighting the listing on the sidebar will be handled separately for the two different click events.) Add these functions to your JavaScript:

function flyToStore(currentFeature) {
map.flyTo({
center: currentFeature.geometry.coordinates,
zoom: 15
});
}

function createPopUp(currentFeature) {
const popUps = document.getElementsByClassName('mapboxgl-popup');
/** Check if there is already a popup on the map and if so, remove it */
if (popUps[0]) popUps[0].remove();

const popup = new mapboxgl.Popup({ closeOnClick: false })
.setLngLat(currentFeature.geometry.coordinates)
.setHTML(`<h3>Sweetgreen</h3><h4>${currentFeature.properties.address}</h4>`)
.addTo(map);
}

Style your popups by adding the following to your CSS:

/* Marker tweaks */
.mapboxgl-popup-close-button {
display: none;
}

.mapboxgl-popup-content {
font:
400 15px/22px 'Source Sans Pro',
'Helvetica Neue',
sans-serif;
padding: 0;
width: 180px;
}

.mapboxgl-popup-content h3 {
background: #91c949;
color: #fff;
margin: 0;
padding: 10px;
border-radius: 3px 3px 0 0;
font-weight: 700;
margin-top: -15px;
}

.mapboxgl-popup-content h4 {
margin: 0;
padding: 10px;
font-weight: 400;
}

.mapboxgl-popup-content div {
padding: 10px;
}

.mapboxgl-popup-anchor-top > .mapboxgl-popup-content {
margin-top: 15px;
}

.mapboxgl-popup-anchor-top > .mapboxgl-popup-tip {
border-bottom-color: #91c949;
}

Add event listeners

Now that you've defined these two functions, you want them to fire when a user clicks on a restaurant in the sidebar listing or when a user clicks on a restaurant on the map. To do this, you will add event listeners that listen for "click" events and execute some function when they happen. You will add two event listeners: one for when a link in the sidebar is clicked and one for when a location on the map is clicked.

In buildLocationList's for..of statement, add this code to handle when a user clicks a link in the sidebar:

link.addEventListener('click', function () {
for (const feature of stores.features) {
if (this.id === `link-${feature.properties.id}`) {
flyToStore(feature);
createPopUp(feature);
}
}
const activeItem = document.getElementsByClassName('active');
if (activeItem[0]) {
activeItem[0].classList.remove('active');
}
this.parentNode.classList.add('active');
});

Before buildLocationList, add this code to handle when a location circle on the map is clicked:

map.on('click', (event) => {
/* Determine if a feature in the "locations" layer exists at that point. */
const features = map.queryRenderedFeatures(event.point, {
layers: ['locations']
});

/* If it does not exist, return */
if (!features.length) return;

const clickedPoint = features[0];

/* Fly to the point */
flyToStore(clickedPoint);

/* Close all other popups and display popup for clicked store */
createPopUp(clickedPoint);

/* Highlight listing in sidebar (and remove highlight for all other listings) */
const activeItem = document.getElementsByClassName('active');
if (activeItem[0]) {
activeItem[0].classList.remove('active');
}
const listing = document.getElementById(
`listing-${clickedPoint.properties.id}`
);
listing.classList.add('active');
});

Save your changes, and reload the page. Now when you click on one of the locations, the map will fly to that store and highlight the correct store listing in the sidebar. If you click on one of the listings in the sidebar, the map will also fly to the correct location.

Add custom markers

This section will walk you through how to replace the existing standard circle layer with custom markers. First, you will need to remove the existing circle layer and related functions. Remove the circle layer by deleting the .addLayer() function from your code, and replace it with the .addSource() code below. Instead of styling the circle layer with addLayer, you will use .addSource() to add an image to each point in the GeoJSON data:

map.addSource('places', {
type: 'geojson',
data: stores
});

Next, delete the map.on('click') function we added in Add event listeners, which listened for a click on a location circle to fly to the location, display a popup, and highlight the list item.

Then, add the custom Sweetgreen icons to the map using mapboxgl.Marker() objects. Unlike the circle layer, which is in the map, mapboxgl.Marker() objects are HTML DOM elements that can be styled with CSS. Add a new class to the CSS called .marker and set the Sweetgreen marker you downloaded earlier as the background-image:

.marker {
border: none;
cursor: pointer;
height: 56px;
width: 56px;
background-image: url('marker.png');
}

To add the new markers to the map, create an addMarkers function to iterate through all stores and add the new marker to the map at each location. Add the following code after the map.on('load') function:

function addMarkers() {
/* For each feature in the GeoJSON object above: */
for (const marker of stores.features) {
/* Create a div element for the marker. */
const el = document.createElement('div');
/* Assign a unique `id` to the marker. */
el.id = `marker-${marker.properties.id}`;
/* Assign the `marker` class to each marker for styling. */
el.className = 'marker';

/**
* Create a marker using the div element
* defined above and add it to the map.
**/
new mapboxgl.Marker(el, { offset: [0, -23] })
.setLngLat(marker.geometry.coordinates)
.addTo(map);
}
}

Then, you will need to call this function when the map loads. After addSource() inside your map.on('load', () => {}) function, add:

addMarkers();

Add new event listeners

Now that you have replaced your circle layer with markers, you will need to re-add some code for flying to the position on the map, displaying a popup, and highlighting the list item in the sidebar when clicking on the marker. Within the for...of statement inside the addMarkers function from above, add an event listener:

el.addEventListener('click', (e) => {
/* Fly to the point */
flyToStore(marker);
/* Close all other popups and display popup for clicked store */
createPopUp(marker);
/* Highlight listing in sidebar */
const activeItem = document.getElementsByClassName('active');
e.stopPropagation();
if (activeItem[0]) {
activeItem[0].classList.remove('active');
}
const listing = document.getElementById(`listing-${marker.properties.id}`);
listing.classList.add('active');
});

Final tweaks

You will need to adjust the position of the popup to account for the added height of the marker. You can do this using CSS:

.mapboxgl-popup {
padding-bottom: 50px;
}

Finished product

Save your work and reload the page. You have completed the store locator using Mapbox GL JS.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Demo: Build a store locator using Mapbox GL JS</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css"
rel="stylesheet"
/>
<style>
body {
color: #404040;
font:
400 15px/22px 'Source Sans Pro',
'Helvetica Neue',
sans-serif;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
}

* {
box-sizing: border-box;
}

.sidebar {
position: absolute;
width: 33.3333%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
border-right: 1px solid rgb(0 0 0 / 25%);
}

.map {
position: absolute;
left: 33.3333%;
width: 66.6666%;
top: 0;
bottom: 0;
}

h1 {
font-size: 22px;
margin: 0;
font-weight: 400;
line-height: 20px;
padding: 20px 2px;
}

a {
color: #404040;
text-decoration: none;
}

a:hover {
color: #101010;
}

.heading {
background: #fff;
border-bottom: 1px solid #eee;
min-height: 60px;
line-height: 60px;
padding: 0 10px;
background-color: #00853e;
color: #fff;
}

.listings {
height: 100%;
overflow: auto;
padding-bottom: 60px;
}

.listings .item {
display: block;
border-bottom: 1px solid #eee;
padding: 10px;
text-decoration: none;
}

.listings .item:last-child {
border-bottom: none;
}

.listings .item .title {
display: block;
color: #00853e;
font-weight: 700;
}

.listings .item .title small {
font-weight: 400;
}

.listings .item.active .title,
.listings .item .title:hover {
color: #8cc63f;
}

.listings .item.active {
background-color: #f8f8f8;
}

::-webkit-scrollbar {
width: 3px;
height: 3px;
border-left: 0;
background: rgb(0 0 0 / 10%);
}

::-webkit-scrollbar-track {
background: none;
}

::-webkit-scrollbar-thumb {
background: #00853e;
border-radius: 0;
}

.marker {
border: none;
cursor: pointer;
height: 56px;
width: 56px;
background-image: url('https://docs.mapbox.com/demos/building-a-store-locator/marker.png');
}

/* Marker tweaks */
.mapboxgl-popup {
padding-bottom: 50px;
}

.mapboxgl-popup-close-button {
display: none;
}

.mapboxgl-popup-content {
font:
400 15px/22px 'Source Sans Pro',
'Helvetica Neue',
sans-serif;
padding: 0;
width: 180px;
}

.mapboxgl-popup-content h3 {
background: #91c949;
color: #fff;
margin: 0;
padding: 10px;
border-radius: 3px 3px 0 0;
font-weight: 700;
margin-top: -15px;
}

.mapboxgl-popup-content h4 {
margin: 0;
padding: 10px;
font-weight: 400;
}

.mapboxgl-popup-content div {
padding: 10px;
}

.mapboxgl-popup-anchor-top > .mapboxgl-popup-content {
margin-top: 15px;
}

.mapboxgl-popup-anchor-top > .mapboxgl-popup-tip {
border-bottom-color: #91c949;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="heading">
<h1>Our locations</h1>
</div>
<div id="listings" class="listings"></div>
</div>
<div id="map" class="map"></div>
<script>
mapboxgl.accessToken = '{{MAPBOX_ACCESS_TOKEN}}';

/**
* Add the map to the page
*/
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v11',
center: [-77.034084142948, 38.909671288923],
zoom: 13,
scrollZoom: false
});

const stores = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.034084142948, 38.909671288923]
},
'properties': {
'phoneFormatted': '(202) 234-7336',
'phone': '2022347336',
'address': '1471 P St NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at 15th St NW',
'postalCode': '20005',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.049766, 38.900772]
},
'properties': {
'phoneFormatted': '(202) 507-8357',
'phone': '2025078357',
'address': '2221 I St NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at 22nd St NW',
'postalCode': '20037',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.043929, 38.910525]
},
'properties': {
'phoneFormatted': '(202) 387-9338',
'phone': '2023879338',
'address': '1512 Connecticut Ave NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at Dupont Circle',
'postalCode': '20036',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.0672, 38.90516896]
},
'properties': {
'phoneFormatted': '(202) 337-9338',
'phone': '2023379338',
'address': '3333 M St NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at 34th St NW',
'postalCode': '20007',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.002583742142, 38.887041080933]
},
'properties': {
'phoneFormatted': '(202) 547-9338',
'phone': '2025479338',
'address': '221 Pennsylvania Ave SE',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'btwn 2nd & 3rd Sts. SE',
'postalCode': '20003',
'state': 'D.C.'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-76.933492720127, 38.99225245786]
},
'properties': {
'address': '8204 Baltimore Ave',
'city': 'College Park',
'country': 'United States',
'postalCode': '20740',
'state': 'MD'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.097083330154, 38.980979]
},
'properties': {
'phoneFormatted': '(301) 654-7336',
'phone': '3016547336',
'address': '4831 Bethesda Ave',
'cc': 'US',
'city': 'Bethesda',
'country': 'United States',
'postalCode': '20814',
'state': 'MD'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.359425054188, 38.958058116661]
},
'properties': {
'phoneFormatted': '(571) 203-0082',
'phone': '5712030082',
'address': '11935 Democracy Dr',
'city': 'Reston',
'country': 'United States',
'crossStreet': 'btw Explorer & Library',
'postalCode': '20190',
'state': 'VA'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.10853099823, 38.880100922392]
},
'properties': {
'phoneFormatted': '(703) 522-2016',
'phone': '7035222016',
'address': '4075 Wilson Blvd',
'city': 'Arlington',
'country': 'United States',
'crossStreet': 'at N Randolph St.',
'postalCode': '22203',
'state': 'VA'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-75.28784, 40.008008]
},
'properties': {
'phoneFormatted': '(610) 642-9400',
'phone': '6106429400',
'address': '68 Coulter Ave',
'city': 'Ardmore',
'country': 'United States',
'postalCode': '19003',
'state': 'PA'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-75.20121216774, 39.954030175164]
},
'properties': {
'phoneFormatted': '(215) 386-1365',
'phone': '2153861365',
'address': '3925 Walnut St',
'city': 'Philadelphia',
'country': 'United States',
'postalCode': '19104',
'state': 'PA'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [-77.043959498405, 38.903883387232]
},
'properties': {
'phoneFormatted': '(202) 331-3355',
'phone': '2023313355',
'address': '1901 L St. NW',
'city': 'Washington DC',
'country': 'United States',
'crossStreet': 'at 19th St',
'postalCode': '20036',
'state': 'D.C.'
}
}
]
};

/**
* Assign a unique id to each store. You'll use this `id`
* later to associate each point on the map with a listing
* in the sidebar.
*/
stores.features.forEach((store, i) => {
store.properties.id = i;
});

/**
* Wait until the map loads to make changes to the map.
*/
map.on('load', () => {
/**
* This is where your '.addLayer()' used to be, instead
* add only the source without styling a layer
*/
map.addSource('places', {
'type': 'geojson',
'data': stores
});

/**
* Add all the things to the page:
* - The location listings on the side of the page
* - The markers onto the map
*/
buildLocationList(stores);
addMarkers();
});

/**
* Add a marker to the map for every store listing.
**/
function addMarkers() {
/* For each feature in the GeoJSON object above: */
for (const marker of stores.features) {
/* Create a div element for the marker. */
const el = document.createElement('div');
/* Assign a unique `id` to the marker. */
el.id = `marker-${marker.properties.id}`;
/* Assign the `marker` class to each marker for styling. */
el.className = 'marker';

/**
* Create a marker using the div element
* defined above and add it to the map.
**/
new mapboxgl.Marker(el, { offset: [0, -23] })
.setLngLat(marker.geometry.coordinates)
.addTo(map);

/**
* Listen to the element and when it is clicked, do three things:
* 1. Fly to the point
* 2. Close all other popups and display popup for clicked store
* 3. Highlight listing in sidebar (and remove highlight for all other listings)
**/
el.addEventListener('click', (e) => {
/* Fly to the point */
flyToStore(marker);
/* Close all other popups and display popup for clicked store */
createPopUp(marker);
/* Highlight listing in sidebar */
const activeItem = document.getElementsByClassName('active');
e.stopPropagation();
if (activeItem[0]) {
activeItem[0].classList.remove('active');
}
const listing = document.getElementById(
`listing-${marker.properties.id}`
);
listing.classList.add('active');
});
}
}

/**
* Add a listing for each store to the sidebar.
**/
function buildLocationList(stores) {
for (const store of stores.features) {
/* Add a new listing section to the sidebar. */
const listings = document.getElementById('listings');
const listing = listings.appendChild(document.createElement('div'));
/* Assign a unique `id` to the listing. */
listing.id = `listing-${store.properties.id}`;
/* Assign the `item` class to each listing for styling. */
listing.className = 'item';

/* Add the link to the individual listing created above. */
const link = listing.appendChild(document.createElement('a'));
link.href = '#';
link.className = 'title';
link.id = `link-${store.properties.id}`;
link.innerHTML = `${store.properties.address}`;

/* Add details to the individual listing. */
const details = listing.appendChild(document.createElement('div'));
details.innerHTML = `${store.properties.city}`;
if (store.properties.phone) {
details.innerHTML += ` &middot; ${store.properties.phoneFormatted}`;
}

/**
* Listen to the element and when it is clicked, do four things:
* 1. Update the `currentFeature` to the store associated with the clicked link
* 2. Fly to the point
* 3. Close all other popups and display popup for clicked store
* 4. Highlight listing in sidebar (and remove highlight for all other listings)
**/
link.addEventListener('click', function () {
for (const feature of stores.features) {
if (this.id === `link-${feature.properties.id}`) {
flyToStore(feature);
createPopUp(feature);
}
}
const activeItem = document.getElementsByClassName('active');
if (activeItem[0]) {
activeItem[0].classList.remove('active');
}
this.parentNode.classList.add('active');
});
}
}

/**
* Use Mapbox GL JS's `flyTo` to move the camera smoothly
* a given center point.
**/
function flyToStore(currentFeature) {
map.flyTo({
center: currentFeature.geometry.coordinates,
zoom: 15
});
}

/**
* Create a Mapbox GL JS `Popup`.
**/
function createPopUp(currentFeature) {
const popUps = document.getElementsByClassName('mapboxgl-popup');
if (popUps[0]) popUps[0].remove();
const popup = new mapboxgl.Popup({ closeOnClick: false })
.setLngLat(currentFeature.geometry.coordinates)
.setHTML(
`<h3>Sweetgreen</h3><h4>${currentFeature.properties.address}</h4>`
)
.addTo(map);
}
</script>
</body>
</html>

Next steps

After following this guide, you have the tools you need to create your own store locator. Explore more Mapbox GL JS resources, including examples and other tutorials, in the Mapbox GL JS documentation.

Was this page helpful?