All docschevron-rightHelpchevron-rightarrow-leftTutorialschevron-rightBuild a store locator using Mapbox.js

Build a store locator using Mapbox.js

Advanced
JavaScript
Prerequisite

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

alert
LEGACY

Mapbox.js is no longer in active development. To learn more about our newer mapping tools see Build a store locator with Mapbox GL JS.

In this tutorial, you'll learn how to create a store locator map. Using your new map, you'll be able to browse through a list of locations from a sidebar and select a specific store to view more information. Selecting a marker on the map will highlight the selected store on the sidebar.

For this tutorial, you're going to use Sweetgreen, a local salad shop, as an example. They have a healthy number of locations, plus their salads are delicious.

This guide gets into a few more advanced JavaScript concepts with Mapbox.js. If you're new to Mapbox.js, we recommend you read our Extending with Mapbox.js guide first.

Getting started

Before writing any code, create a local folder called "building-a-store-locator-js" to house your project files. This folder is referred to as your project folder from here on out.

There are a few resources you'll need to gather before we get started:

  • A tileset ID. A tileset ID points to a unique map you have created with Mapbox. For the purposes of this guide, you'll use the tileset ID mapbox.k8xv42t9, but you can substitute your own custom tileset if you like!
  • Your access token. The token is used to associate a map with your account:
L.mapbox.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
  • Mapbox.js. The Mapbox JavaScript API for building maps.

  • Data. We collected Sweetgreen's locations and marked up the data in GeoJSON for your convenience.

  • Custom map marker. Your finished map will have some fancy custom images for its markers. Download and save the image to your project folder.

arrow-downDownload custom marker
  • A text editor. You'll be writing HTML, CSS, and JavaScript after all.

Add structure

In your project folder, create an index.html file. Set up the document by adding reference the Mapbox.js JavaScript library and its accompanying CSS:

<script src='https://api.mapbox.com/mapbox.js/v3.2.1/mapbox.js'></script>
<link href='https://api.mapbox.com/mapbox.js/v3.2.1/mapbox.css' rel='stylesheet' />

Next, markup the page to create a map container and sidebar listing:

<div class='sidebar pad2'>Listing</div>
<div id='map' class='map pad2'>Map</div>

Apply a bit of CSS so you can visualize what the layout looks like:

body {
  background: #404040;
  color: #f8f8f8;
  font: 500 20px/26px 'Helvetica Neue', Helvetica, Arial, sans-serif;
  margin: 0;
  padding: 0;
  -webkit-font-smoothing: antialiased;
}

/**
 * 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 {
  width: 33.3333%;
}

.map {
  border-left: 1px solid #fff;
  position: absolute;
  left: 33.3333%;
  width: 66.6666%;
  top: 0;
  bottom: 0;
}

.pad2 {
  padding: 20px;
  box-sizing: border-box;
}

Initialize map

Now that you have set up the structure of the project, bring your map to life by initializing it with Mapbox.js.

First, create an instance of L.mapbox.map and store it in a variable called map. You'll also use setView to focus the map to Washington DC where all the shops are located. Here, you'll need to update your tileset ID and access token. Add the script under your HTML:

L.mapbox.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
const map = L.mapbox
  .map('map')
  .setView([38.909671288923, -77.034084142948], 13)
  .addLayer(L.mapbox.styleLayer('mapbox://styles/mapbox/light-v11'));

Load data

Before you put any markers on your map, store all the GeoJSON data you downloaded above in a variable called geojson. Once the map is initialized, you can use featureLayer.setGeoJSON to load features on the map by referencing the geojson variable:

L.mapbox.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
const geojson = {
  "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."
      }
    }
  ]
};
const map = L.mapbox
  .map('map')
  .setView([38.909671288923, -77.034084142948], 13)
  .addLayer(L.mapbox.styleLayer('mapbox://styles/mapbox/light-v11'))
  .featureLayer.setGeoJSON(geojson);

Alternatively, you can save the GeoJSON as a .geojson file and load the file on to the map. If you do this, you will need to run your application from a local web server (something like http://localhost/building-a-store-locator-js.html). If you try to open your index.html file directly in your browser, you will receive a Cross-origin Resource Sharing (CORS) error.

Build listings and tooltips

You have some markers on the map, but you still need to build the listings and tooltips. Rather than creating each manually, you can put JavaScript to work to read through the GeoJSON and dynamically build listings and set tooltips for us. This means that if you need to add a location then you only need to update the GeoJSON.

L.mapbox.featureLayer allows you to iterate over data before it is added to the map using the eachLayer method. This means that JavaScript will run through each point, populate the custom tooltip, add each location listing to the sidebar, and listen for and respond to a user's click event to track when a marker or listing is selected:

const locations = L.mapbox.featureLayer().addTo(map);
locations.setGeoJSON(geojson);
locations.eachLayer((locale) => {
  // Iterate over each marker.
});

The variable locations references your new L.mapbox.featureLayer, then taps into eachLayer. The passed parameter locale is each marker object.

At this point you can start populating content into each listing using information provided by each locale. Nice! But you still need to update the sidebar HTML to hold the listing information:

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

Next, update your CSS to accommodate your layout changes:

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

* {
  box-sizing: border-box;
}

h1 {
  font-size: 22px;
  margin: 0;
  font-weight: 400;
}

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

a:hover {
  color: #101010;
}

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

.pad2 {
  padding: 20px;
}

.quiet {
  color: #888;
}

.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;
}

.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;
}

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

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

.leaflet-popup-content-wrapper {
  padding: 0;
}

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

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

.leaflet-container .leaflet-marker-icon {
  cursor: pointer;
}

Now you can put the finishing touches on your script. Outside of eachLayer, create a variable called listings that selects the listing container in the HTML. With this selected you can append each listing into the sidebar:

const listings = document.getElementById('listings');

locations.eachLayer((locale) => {
  // Shorten locale.feature.properties to `prop` so we're not
  // writing this long form over and over again.
  const prop = locale.feature.properties;

  const listing = listings.appendChild(document.createElement('div'));
  listing.className = 'item';

  const link = listing.appendChild(document.createElement('a'));
  link.href = '#';
  link.className = 'title';
  link.innerHTML = `${prop.address}`;

  if (prop.crossStreet) {
    link.innerHTML += ` <br /><small>${prop.crossStreet}</small>`;
  }

  const details = listing.appendChild(document.createElement('div'));
  details.innerHTML = `${prop.city}`;

  if (prop.phone) {
    details.innerHTML += ` · ${prop.phoneFormatted}`;
  }
});

The link variable should have the special behavior of panning to its associated marker on the map. By binding an onclick event, you can target the current locale object in context, pan the map to the object's coordinates, and trigger its tooltip to appear.

link.onclick = function () {
  // 1. Toggle an active class for `listing`. View the source in the demo link for example.
  // 2. When a menu item is clicked, animate the map to center
  // its associated locale and open its popup.
  map.setView(locale.getLatLng(), 16);
  locale.openPopup();
};

Each marker tooltip requires unique information. Use the bindPopup method to pass content to your popups.

const popup = 'Sweetgreen';
locale.bindPopup(popup);

Finally, make sure that when a user clicks a marker that it sets the associated listing in the sidebar as active by binding a click event handler to each locale.

locale.on('click', () => {
  // 1. Toggle an active class for `listing`. View the source in the demo link for example.

  // 2. center the map on the selected marker.
  map.setView(locale.getLatLng(), 16);
});

At this point, your whole script should look like:

const listings = document.getElementById('listings');
const locations = L.mapbox.featureLayer().addTo(map);

locations.setGeoJSON(geojson);

function setActive(el) {
  const siblings = listings.getElementsByTagName('div');
  for (const sibling of siblings) {
    sibling.classList.remove('active');
  }

  el.classList.add('active');
}

locations.eachLayer((locale) => {
  // Shorten locale.feature.properties to `prop` so you don't
  // have to write this long form over and over again.
  const prop = locale.feature.properties;

  // Each marker on the map.
  let popup = `<h3>Sweetgreen</h3><div>${prop.address}`;

  const listing = listings.appendChild(document.createElement('div'));
  listing.className = 'item';

  const link = listing.appendChild(document.createElement('a'));
  link.href = '#';
  link.className = 'title';

  link.innerHTML = `${prop.address}`;
  if (prop.crossStreet) {
    link.innerHTML += `<br /><small class="quiet">${prop.crossStreet}</small>`;
    popup += `<br /><small class="quiet">${prop.crossStreet}</small>`;
  }

  const details = listing.appendChild(document.createElement('div'));
  details.innerHTML = `${prop.city}`;
  if (prop.phone) {
    details.innerHTML += ` · ${prop.phoneFormatted}`;
  }

  link.onclick = function () {
    setActive(listing);

    // When a menu item is clicked, animate the map to center
    // its associated locale and open its popup.
    map.setView(locale.getLatLng(), 16);
    locale.openPopup();
    return false;
  };

  // Marker interaction
  locale.on('click', () => {
    // 1. center the map on the selected marker.
    map.panTo(locale.getLatLng());

    // 2. Set active the markers associated listing.
    setActive(listing);
  });

  locale.bindPopup(popup);
});

There's a lot of code inside the eachLayer function because it makes sure each event is bound to its respective elements.

Add custom markers

You could add style to our markers by adding property keys to each feature in the GeoJSON, but for this example you want to give your stores unique and fancy markers.

To give your markers a custom icon, use the setIcon function inside the locations.eachLayer function you created earlier. In setIcon, pass L.icon with your own custom options by copying and pasting the following right after locale.bindPopup(popup);:

locale.setIcon(
  L.icon({
    iconUrl: 'marker.png',
    iconSize: [56, 56],
    iconAnchor: [28, 28],
    popupAnchor: [0, -34]
  })
);

Freshen up your map's type with the Source Sans Pro font:

<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700' rel='stylesheet'>

You'll also need to update the font property in body style:

font:400 15px/22px 'Source Sans Pro', 'Helvetica Neue', sans-serif;

Finished product

You have completed the store locator!

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Demo: Build a store locator using Mapbox.js</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700"
rel="stylesheet"
/>
<script src="https://api.mapbox.com/mapbox.js/v3.2.1/mapbox.js"></script>
<link
href="https://api.mapbox.com/mapbox.js/v3.2.1/mapbox.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;
}
h1 {
font-size: 22px;
margin: 0;
font-weight: 400;
}
a {
color: #404040;
text-decoration: none;
}
a:hover {
color: #101010;
}
.sidebar {
position: absolute;
width: 33.3333%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
border-right: 1px solid rgba(0, 0, 0, 0.25);
}
.pad2 {
padding: 20px;
}
.quiet {
color: #888;
}
.map {
position: absolute;
left: 33.3333%;
width: 66.6666%;
top: 0;
bottom: 0;
}
.heading {
background: #fff;
border-bottom: 1px solid #eee;
min-height: 60px;
line-height: 60px;
padding: 0 10px;
}
.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;
}
/* Marker tweaks */
.leaflet-popup-close-button {
display: none;
}
.leaflet-popup-content {
font: 400 15px/22px 'Source Sans Pro', 'Helvetica Neue', sans-serif;
padding: 0;
width: 200px;
}
.leaflet-popup-content-wrapper {
padding: 0;
}
.leaflet-popup-content h3 {
background: #91c949;
color: #fff;
margin: 0;
display: block;
padding: 10px;
border-radius: 3px 3px 0 0;
font-weight: 700;
margin-top: -15px;
}
.leaflet-popup-content div {
padding: 10px;
}
.leaflet-container .leaflet-marker-icon {
cursor: pointer;
}
</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>
L.mapbox.accessToken = '<your access token here>';
const geojson = [
{
'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.'
}
}
]
}
];
const map = L.mapbox
.map('map')
.setView([38.909671288923, -77.034084142948], 16)
.addLayer(L.mapbox.styleLayer('mapbox://styles/mapbox/light-v11'));
map.scrollWheelZoom.disable();
const listings = document.getElementById('listings');
const locations = L.mapbox.featureLayer().addTo(map);
locations.setGeoJSON(geojson);
function setActive(el) {
const siblings = listings.getElementsByTagName('div');
for (const sibling of siblings) {
sibling.classList.remove('active');
}
el.classList.add('active');
}
locations.eachLayer((locale) => {
// Shorten locale.feature.properties to just `prop` so we're not
// writing this long form over and over again.
const prop = locale.feature.properties;
// Each marker on the map.
let popup = `<h3>Sweetgreen</h3><div>${prop.address}`;
const listing = listings.appendChild(document.createElement('div'));
listing.className = 'item';
const link = listing.appendChild(document.createElement('a'));
link.href = '#';
link.className = 'title';
link.innerHTML = `${prop.address}`;
if (prop.crossStreet) {
link.innerHTML += `<br /><small class="quiet">${prop.crossStreet}</small>`;
popup += `<br /><small class="quiet">${prop.crossStreet}</small>`;
}
const details = listing.appendChild(document.createElement('div'));
details.innerHTML = `${prop.city}`;
if (prop.phone) {
details.innerHTML += ` &middot; ${prop.phoneFormatted}`;
}
link.onclick = function () {
setActive(listing);
// When a menu item is clicked, animate the map to center
// its associated locale and open its popup.
map.setView(locale.getLatLng(), 16);
locale.openPopup();
return false;
};
// Marker interaction
locale.on('click', () => {
// 1. center the map on the selected marker.
map.panTo(locale.getLatLng());
// 2. Set active the markers associated listing.
setActive(listing);
});
locale.bindPopup(popup);
locale.setIcon(
L.icon({
iconUrl: 'marker.png',
iconSize: [56, 56],
iconAnchor: [28, 28],
popupAnchor: [0, -34]
})
);
});
</script>
</body>
</html>

If you're interested in loading your GeoJSON from a file, you can download the alternative code below. Heads up! You'll need to serve these files to avoid a CORS error.

arrow-downDownload the alternative code

Next steps

Nice job! We hope you're set with the tools to create your own store locator.

For bonus points, if a store's location data is added to Foursquare you could extend this example even further by pulling data directly from Foursquare's API. API requests return geolocated information and details about a place. This would allow for more dynamic data like user submitted images or the total number of check-ins a location has received.