I like to write almost as much as I like to build. I write about dashboards, maps, freelancing, music, and the outdoors. If this interests you too, please join my mailing list to get updates whenever I publish new content.

Subscribe

How to Create a Map Popup Component Using Mapbox and React

Date: 4/25/2021

Time to Read: 5 minutes

Mapbox and React Popups

This post is part of my Building Interactive Maps with React course - a course for anyone wanting to learn how to build interactive maps and integrate them into their React applications. If you enjoy this guide, then chances are you will enjoy the course too!

What You Will Learn

Interactivity is part of what makes web maps so compelling. A relatively easy way to add interactivity to a web map is by adding popups to display information about your map layers. This guide will teach you how to add popups to a web map using React and Mapbox GL JS.

Introduction

If I am building a map for a client, one of the first things they typically ask is "Can it have popups?" As such, understanding how to quickly build and style map popups has become an essential part of my development workflow. Luckily the process is relatively straight forward and React and Mapbox GL JS play very nicely together. For this guide, we will be building a simple map showcasing bus routes for Salt Lake City, Utah and creating a simple map popup that shows some info about a route when a user clicks on it.

Getting Started

This guide assumes you have moved through the other guides that are part of this course and are now comfortable creating an interactive map using Mapbox GL JS and React.

For ease of use, we are going to use Create React App to get our application up and running. If you would like to follow along, I have put together both a Code Sandbox and an example repository which you can find here.

To begin, create a new application using Create React App and navigate into the project.

1
npx create-react-app mapbox-and-popups
2
cd mapbox-and-popups
3

In the root of the project, create a new .env file and copy your Mapbox access token from your account page. If you need a refresher on how to get your access token, check out the Introduction to Mapbox and React guide. Then create a new entry in the .env file titled REACT_APP_MAPBOX_TOKEN and set its value equal to the access token you copied to your clipboard. It is generally best practice to store sensitive information like access tokens in a .env file and keep them out of version control.

1
REACT_APP_MAPBOX_TOKEN=<YOUR_TOKEN_HERE>
2

Next, we need to add the Mapbox GL JS library to our project as a dependency.

1
# yarn
2
yarn add mapbox-gl
3
4
# npm
5
npm install mapbox-gl
6

Setting Up the Map

As mentioned earlier, we will be creating an interactive map that shows bus routes in Salt Lake City and displays a popup with some some information on the bus route when the user clicks on a route. This tutorial assumes you already know how to setup a basic map and add a source and layers to it. If you are not familiar with this workflow, please review the Introduction to Mapbox and React guide and the A Complete Guide to Sources and Layers in React and Mapbox GL JS guide before continuing.

For the sake of simplicity, I will be setting up the map in the root of the src directory in the App.js file. The following snippet does the following:

  • creates a basic interactive map
  • adds a spatial data source for the bus routes using an ESRI API
  • adds a line layer to the map for visualizing the bus routes
1
import React, { useRef, useEffect } from "react"
2
import ReactDOM from "react-dom"
3
import mapboxgl from "mapbox-gl"
4
// import the mapbox styles
5
// alternatively can use a link tag in the head of public/index.html
6
// see https://docs.mapbox.com/mapbox-gl-js/api/
7
import "mapbox-gl/dist/mapbox-gl.css"
8
import "./App.css"
9
10
// Grab the access token from your Mapbox account
11
// I typically like to store sensitive things like this
12
// in a .env file
13
mapboxgl.accessToken = process.env.REACT_APP_MAPBOX_TOKEN
14
15
const App = () => {
16
const mapContainer = useRef()
17
18
// this is where all of our map logic is going to live
19
// adding the empty dependency array ensures that the map
20
// is only rendered once
21
useEffect(() => {
22
// create the map and configure it
23
// check out the API reference for more options
24
// https://docs.mapbox.com/mapbox-gl-js/api/map/
25
const map = new mapboxgl.Map({
26
container: mapContainer.current,
27
style: "mapbox://styles/mapbox/outdoors-v11",
28
center: [-111.94, 40.611],
29
zoom: 12,
30
})
31
32
// only want to work with the map after it has fully loaded
33
// if you try to add sources and layers before the map has loaded
34
// things will not work properly
35
map.on("load", () => {
36
// bus routes source
37
// another example of using a geojson source
38
// this time we are hitting an ESRI API that returns
39
// data in the geojson format
40
// see https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#geojson
41
map.addSource("bus-routes", {
42
type: "geojson",
43
data:
44
"https://opendata.arcgis.com/datasets/4347f3565fbe4d5dbb97b016768b8907_0.geojson",
45
})
46
47
// bus routes - line layer
48
// see https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#line
49
map.addLayer({
50
id: "bus-routes-line",
51
type: "line",
52
source: "bus-routes",
53
paint: {
54
"line-color": "#4094ae",
55
"line-width": 4,
56
},
57
})
58
})
59
60
// cleanup function to remove map on unmount
61
return () => map.remove()
62
}, [])
63
64
return <div ref={mapContainer} style={{ width: "100%", height: "100vh" }} />
65
}
66
67
export default App
68

Deciding What to Show in the Popup

Now that we have the bus routes displaying on our map, it is time to add some interactivity. The goal is to display some information about a bus route when a user clicks on the route on the map. To accomplish this we will be creating a Popup component and then adding some logic to render the Popup component with information about the route the user clicked on.

Each bus route has some basic metadata. The snippet below highlights the type of data that is available for each route.

1
{
2
"FID": 96,
3
"LineAbbr": "F578",
4
"LineName": "7800 S FLEX",
5
"Frequency": "30",
6
"RouteType": "Flex",
7
"City": "Midvale, West Jordan",
8
"County": "Salt Lake",
9
"AvgBrd": 101,
10
"SHAPE_Length": 0.14346444450222645
11
}
12

The info that is likely most relevant for the user are the LineAbbr, LineName, RouteType, and City properties. Accordingly, we will setup the map popup to display the data for these fields.

Creating the Popup

Now that we have decided what information we would like to highlight, we can go about creating our Popup component. For ease of demonstration, I am going to create the Popup component in our existing App.js file versus breaking the component out into its own file.

Here is the full snippet for creating the Popup component and getting it wired up to the map. If you are just looking for a snippet, feel free to copy and paste and go on your way. Otherwise, I will continue with a detailed explanation below.

1
import React, { useRef, useEffect } from "react"
2
import ReactDOM from "react-dom"
3
import mapboxgl from "mapbox-gl"
4
// import the mapbox styles
5
// alternatively can use a link tag in the head of public/index.html
6
// see https://docs.mapbox.com/mapbox-gl-js/api/
7
import "mapbox-gl/dist/mapbox-gl.css"
8
import "./App.css"
9
10
// Grab the access token from your Mapbox account
11
// I typically like to store sensitive things like this
12
// in a .env file
13
mapboxgl.accessToken = process.env.REACT_APP_MAPBOX_TOKEN
14
15
/**
16
* Our custom Popup component used to render a nicely styled
17
* map popup with additional information about the
18
* user's selected bus route
19
*/
20
const Popup = ({ routeName, routeNumber, city, type }) => (
21
<div className="popup">
22
<h3 className="route-name">{routeName}</h3>
23
<div className="route-metric-row">
24
<h4 className="row-title">Route #</h4>
25
<div className="row-value">{routeNumber}</div>
26
</div>
27
<div className="route-metric-row">
28
<h4 className="row-title">Route Type</h4>
29
<div className="row-value">{type}</div>
30
</div>
31
<p className="route-city">Serves {city}</p>
32
</div>
33
)
34
35
const App = () => {
36
const mapContainer = useRef()
37
const popUpRef = useRef(new mapboxgl.Popup({ offset: 15 }))
38
39
// this is where all of our map logic is going to live
40
// adding the empty dependency array ensures that the map
41
// is only rendered once
42
useEffect(() => {
43
// create the map and configure it
44
// check out the API reference for more options
45
// https://docs.mapbox.com/mapbox-gl-js/api/map/
46
const map = new mapboxgl.Map({
47
container: mapContainer.current,
48
style: "mapbox://styles/mapbox/outdoors-v11",
49
center: [-111.94, 40.611],
50
zoom: 12,
51
})
52
53
// only want to work with the map after it has fully loaded
54
// if you try to add sources and layers before the map has loaded
55
// things will not work properly
56
map.on("load", () => {
57
// bus routes source
58
// another example of using a geojson source
59
// this time we are hitting an ESRI API that returns
60
// data in the geojson format
61
// see https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#geojson
62
map.addSource("bus-routes", {
63
type: "geojson",
64
data:
65
"https://opendata.arcgis.com/datasets/4347f3565fbe4d5dbb97b016768b8907_0.geojson",
66
})
67
68
// bus routes - line layer
69
// see https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#line
70
map.addLayer({
71
id: "bus-routes-line",
72
type: "line",
73
source: "bus-routes",
74
paint: {
75
"line-color": "#4094ae",
76
"line-width": 4,
77
},
78
})
79
})
80
81
/**
82
* Event handler for defining what happens when a user clicks on the map
83
* In this example, we are checking if the user has clicked on a bus route
84
* If they have, we want to render a popup with the data for the selected
85
* bus route
86
* Else, do nothing
87
*/
88
map.on("click", e => {
89
const features = map.queryRenderedFeatures(e.point, {
90
layers: ["bus-routes-line"],
91
})
92
if (features.length > 0) {
93
const feature = features[0]
94
// create popup node
95
const popupNode = document.createElement("div")
96
ReactDOM.render(
97
<Popup
98
routeName={feature?.properties?.LineName}
99
routeNumber={feature?.properties?.LineAbbr}
100
city={feature?.properties?.City}
101
type={feature?.properties?.RouteType}
102
/>,
103
popupNode
104
)
105
popUpRef.current
106
.setLngLat(e.lngLat)
107
.setDOMContent(popupNode)
108
.addTo(map)
109
}
110
})
111
112
// cleanup function to remove map on unmount
113
return () => map.remove()
114
}, [])
115
116
return <div ref={mapContainer} style={{ width: "100%", height: "100vh" }} />
117
}
118
119
export default App
120

The Popup Component

Let's start with the Popup component.

1
/**
2
* Our custom Popup component used to render a nicely styled
3
* map popup with additional information about the
4
* user's selected bus route
5
*/
6
const Popup = ({ routeName, routeNumber, city, type }) => (
7
<div className="popup">
8
<h3 className="route-name">{routeName}</h3>
9
<div className="route-metric-row">
10
<h4 className="row-title">Route #</h4>
11
<div className="row-value">{routeNumber}</div>
12
</div>
13
<div className="route-metric-row">
14
<h4 className="row-title">Route Type</h4>
15
<div className="row-value">{type}</div>
16
</div>
17
<p className="route-city">Serves {city}</p>
18
</div>
19
)
20

The component is super simple and only expects to be passed props for the routeName, routeNumber, city, and type. We then write some basic JSX to control how the provided information is structured and then add class names to control the information is styled. Because we are leveraging the Mapbox GL JS Popup class, we really do not have do much in terms of styling and functionality to get the popup displaying as desired. Mapbox handles all of the hard stuff like the popup positioning and base styles for us.

The base popup styles are pretty boring though so I decided to spruce them up a bit for this guide. Here are the css rules that are being loaded in from an App.css file in my application.

1
.mapboxgl-popup-content {
2
min-width: 200px;
3
}
4
5
.route-metric-row {
6
align-items: center;
7
background-color: #fafafa;
8
border: 1px solid #ddd;
9
border-radius: 16px;
10
display: flex;
11
justify-content: space-between;
12
margin: 8px 0;
13
padding-right: 4px 12px;
14
}
15
16
.row-title {
17
align-items: center;
18
background-color: #4094ae;
19
border: 1px solid #4094ae;
20
border-radius: 16px;
21
color: #ffffff;
22
font-weight: normal;
23
justify-content: center;
24
margin: 0;
25
padding: 4px 12px;
26
text-align: center;
27
}
28
29
.row-value {
30
color: rgba(0, 0, 0, 0.7);
31
font-size: 0.875rem;
32
padding-right: 16px;
33
text-align: right;
34
}
35
36
.route-name {
37
color: rgba(0, 0, 0, 0.7);
38
font-size: 0.875rem;
39
margin: 0;
40
}
41
42
.route-city {
43
background-color: #fafafa;
44
border: 1px solid #ddd;
45
border-radius: 4px;
46
color: rgba(0, 0, 0, 0.65);
47
font-size: 0.8rem;
48
margin-bottom: 0;
49
padding: 8px;
50
}
51

Creating a Popup Instance

The next step in this process is initializing a new Mapbox GL JS popup instance and storing it in a ref. The Popup constructor can take a variety of configuration options. For our purposes, we are leveraging the offset option to ensure that the popup renders 15px away from the feature the user has clicked on. For a complete list of the available configuration options please check out the Mapbox Popup docs.

1
const popUpRef = useRef(new mapboxgl.Popup({ offset: 15 }))
2

Rendering the Popup When the User Clicks on a Route

After creating our Popup component and a new instance of the Mapbox Popup class, we need to add logic to our map that tells Mapbox to display the Popup component when a user clicks on a bus route.

We begin by adding an event handler for when the user clicks on the map. You can imagine the laundry list of things you could do when a user clicks on the map, but for our purposes we want to focus our attention on detecting if the user has clicked on the bus routes layer.

Mapbox provides a handy method on the Map class called queryRenderedFeatures that allows you to query the map for any map features that exist at a provided latitude and longitude. When a user clicks on the map, we get a special Mapbox event object that contains a latitude and longitude among other things that, we can easily pass to the queryRenderedFeatures method to determine if the user has clicked on the bus routes layers. The queryRenderedFeatures method also takes an optional options object that we will leverage to only query the map for features in the bus-routes-line layer. You can check out the Mapbox docs for more information on the method.

If the user has clicked on a bus route, the queryRenderedFeatures method will return an array of features, otherwise it will return an empty array. If the user did indeed click on a feature, we then want to execute a block of logic to create a new div, render the Popup component to it, and add it to the map at the latitude and longitude where the user clicked the map.

1
/**
2
* Event handler for defining what happens when a user clicks on the map
3
* In this example, we are checking if the user has clicked on a bus route
4
* If they have, we want to render a popup with the data for the selected
5
* bus route
6
* Else, do nothing
7
*/
8
map.on("click", e => {
9
const features = map.queryRenderedFeatures(e.point, {
10
layers: ["bus-routes-line"],
11
})
12
if (features.length > 0) {
13
const feature = features[0]
14
// create popup node
15
const popupNode = document.createElement("div")
16
ReactDOM.render(
17
<Popup
18
routeName={feature?.properties?.LineName}
19
routeNumber={feature?.properties?.LineAbbr}
20
city={feature?.properties?.City}
21
type={feature?.properties?.RouteType}
22
/>,
23
popupNode
24
)
25
popUpRef.current
26
.setLngLat(e.lngLat)
27
.setDOMContent(popupNode)
28
.addTo(map)
29
}
30
})
31

Next Steps

This all may seem like a lot initially (at least it was for me), but it becomes an easier design pattern that you will get used to like. The Popup component we built in this guide is relatively simple but React makes it possible to build some very complex and powerful popups will a relatively low amount of effort. For instance, I have had to add images, links, and event charts to popups and each of these use cases was a relatively minor lift. If you are hungering for more after this guide, I recommend trying one of these use cases next!

If you found thus post useful, give me a follow on Twitter or consider picking up a copy of the Building Interactive Maps with React course.

Useful Resources

Join the Newsletter

I periodically send out a newsletter. Interested? Sign up below. You can unsubscribe at any time.

Interested in working together?

Drop me a line
© 2021 Lost Creek Designs