Build a store locator using Mapbox.js

We’re going to create a store locator map. You’ll be able to browse all of the 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.

We’ll use Sweetgreen, a local salad shop, as an example. They have a healthy number of locations, plus their salads are delicious.

This guide takes a deeper dive into JavaScript with Mapbox.js to build an interactive web map. If you’re new to Mapbox.js you might want to check out Extending with Mapbox.js first.

Getting started

For this project, we recommend that you create a local folder called “store-locator” to house your project files. We’ll refer to this folder as your project folder.

There are a few resources you’ll need before we get started:

L.mapbox.accessToken = '<your access token here>';
  • Mapbox.js. The Mapbox JavaScript API for building maps.
  • Data. We collected Sweetgreen’s locations and marked up the data in GeoJSON.

  • Custom map marker. We’ll be using an image for our map marker. Save the image to your project folder.
  • 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 Mapbox.js and CSS.

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

Next, let’s 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>

Let’s also apply some CSS so we 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;
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}

View demo one.

Initialize map

Now that we have the structure of the project, let’s initialize the map with Mapbox.js.

We’ll create a variable of L.mapbox.map called map. We’ll also use setView to focus the map to Washington DC where all the shops are located. Here, you’ll need to update your map ID and access token. Add the script under your HTML.

L.mapbox.accessToken = '<your access token here>';
var map = L.mapbox.map('map', 'mapbox.k8xv42t9')
.setView([38.909671288923, -77.034084142948], 13);

Load data

Let’s store all of the GeoJSON into an array called geojson. With the map initialized, we can use featureLayer.setGeoJSON to load features on the map by referencing the geojson array.

L.mapbox.accessToken = '<your access token here>';
var geojson = {
  /* data from sweetgreen.geojson, downloaded above */
};
var map = L.mapbox.map('map', 'mapbox.k8xv42t9')
.setView([38.909671288923, -77.034084142948], 13)
.featureLayer.setGeoJSON(geojson);

View demo two.

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 this application from a local web server (http://localhost/store-locator.html) otherwise, you will receive a Cross-origin Resource Sharing (CORS) error.

Build listings and tooltips

We have the markers on the map, but we still need to build the listings and tooltips. We can make JavaScript read through the GeoJSON to dynamically build the listings and set the tooltips for us. This means that if you need to add a location then you only need to update the GeoJSON.

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

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

We created the variable locations to reference our new L.mapbox.featureLayer. We’ll use it to tap into eachLayer. The passed parameter locale represents each marker object.

At this point we’re ready to populate content into a listing using information provided by each locale. We 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>

Also, let’s update our styles to accommodate our layout changes:

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

* {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  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 {
  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: rgba(0, 0, 0, 0.1);
}

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

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

.clearfix {
  display: block;
}

.clearfix::after {
  content: '.';
  display: block;
  height: 0;
  clear: both;
  visibility: hidden;
}

/* 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 we can finish writing the script. Outside of eachLayer, let’s create a variable called listings that selects the listing container in the HTML. With this selected we can append each listing into the sidebar.

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

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

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

  var 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>';
  }

  var details = listing.appendChild(document.createElement('div'));
  details.innerHTML = prop.city;

  if (prop.phone) {
    details.innerHTML += ' &middot; ' + prop.phoneFormatted;
  }
});

We want the link variable to have the special behavior of panning to its associated marker on the map. By binding an onclick we 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. Calling bindPopup allows us to pass content that could be any information provided by prop or HTML markup.

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

Finally, we’ll make sure that when a user clicks a marker that it sets the associated listing in the sidebar as active. This locale option has a click event handler attached to it.

locale.on('click', function() {
  // 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);
});

All that new script put together looks like:

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

locations.setGeoJSON(geojson);

function setActive(el) {
  var siblings = listings.getElementsByTagName('div');
  for (var i = 0; i < siblings.length; i++) {
    siblings[i].className = siblings[i].className
    .replace(/active/, '').replace(/\s\s*$/, '');
  }

  el.className += ' active';
}

locations.eachLayer(function(locale) {

  // Shorten locale.feature.properties to just `prop` so we're not
  // writing this long form over and over again.
  var prop = locale.feature.properties;

  // Each marker on the map.
  var popup = '<h3>Sweetgreen</h3><div>' + prop.address;

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

  var 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>';
  }

  var 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', function(e) {
    // 1. center the map on the selected marker.
    map.panTo(locale.getLatLng());

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

  popup += '</div>';
  locale.bindPopup(popup);
});

View demo three.

There’s a lot of build and interaction contained within the eachLayer function because it ensures events are bound to their respective elements.

Add custom markers

We could add style to our markers by adding property keys to each feature in the GeoJSON, but for this example we want to give our store unique markers.

We’ll use an image for our markers:

We can use the setIcon function inside the locations.eachLayer. In setIcon we pass L.icon with our own custom options. Copy and paste the following right after locale.bindPopup(popup);:

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

Let’s also freshened up the type with Source Sans Pro.

<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

Check out our finished store locator!

View demo four.

Download the completed project

If you’re interested in loading your GeoJSON from a file, here’s the slightly modified code. Heads up! You’ll need to serve these files to avoid a CORS error.

Mission complete

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.

Additional questions? Ask our support team or learn more about How Mapbox Works.