Create interactive hover effects with Mapbox GL JS
In this tutorial, you will create a map that shows the locations of earthquakes that have happened in the last week. You will use Mapbox GL JS to create the map and visualize the data, then you will use expressions to style each earthquake feature according to the magnitude of the earthquake. Finally, you will use feature state to apply these styles to the earthquake features when a user mouses over the feature and remove them when a user mouses away.
Getting started
To complete this tutorial, you will need:
- A Mapbox access token. Your Mapbox access tokens are on your Account page.
- Mapbox GL JS. Mapbox GL JS is a JavaScript API for building web maps.
- A text editor. Use the text editor of your choice for writing HTML, CSS, and JavaScript.
- The USGS Earthquake Catalog API. You will use the USGS Earthquake Catalog API to retrieve information about all earthquakes with a magnitude of one or more that have happened within the past week.
Feature state and expressions
Feature state is a set of user-defined attributes that can be dynamically assigned to a feature on the map. You can use feature state with expressions to style the features of a vector or GeoJSON source in either a dataset or tileset. Each feature in the source must have a unique numeric id
for feature state to work correctly. If you are adding a GeoJSON source at runtime, which is what you will do in this tutorial, you can use the generateId
option in map.addSource()
to add an id
to each feature. For vector sources or GeoJSON sources that are added before runtime, you must set a unique id
for each feature before the source is added to the map.
You can think of feature state as a switch that turns a fan on or off. You will use feature state to define what the switch is, and expressions to set the style. In this case, the trigger is a user mousing over or away from a feature, which will update the style of the feature based on the rules set by the expressions.
With feature state, you can update the styling of a map layer's individual features based on user interaction, without needing to re-render the underlying geometry and data after each interaction event.
In this tutorial, the feature state trigger will be a user's mouse hovering over an earthquake feature, and you will use feature state to update the radius size and the color of the feature based on its magnitude value.
Create a map
To get started, you will create a map using Mapbox GL JS. Open your text editor and create a new file named index.html
. Set up this new HTML file by pasting the following code into your text editor:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Create interactive hover effects with 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 {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN ';
const map = new mapboxgl.Map({
container: 'map', // Specify the container ID
style:
'mapbox://styles/mapbox/outdoors-v12', // Specify which map style to use
center: [-122.44121, 37.76132], // Specify the starting position [lng, lat]
zoom: 3.5 // Specify the starting zoom
});
</script>
</body>
</html>
This code creates the structure of the page. It imports Mapbox GL JS in the <head>
of the page, which allows you to use Mapbox GL JS functionality and style in your web app.
This code contains a <div>
element with the ID map
in the <body>
of the page. This <div>
is the container in which the map will be displayed on the page.
Save your changes. Open the HTML file in your browser to see the rendered map, which is centered on the western United States.
Add the sidebar
In the <body>
of your HTML, add a new <div>
. This <div>
will be used to display an individual earthquake's magnitude, location, and the time that it happened:
<div class="quake-info">
<div><strong>Magnitude:</strong> <span id="mag"></span></div>
<div><strong>Location:</strong> <span id="loc"></span></div>
<div><strong>Date:</strong> <span id="date"></span></div>
</div>
To style the new <div>
, add the following CSS to the <style>
section of your HTML:
.quake-info {
position: absolute;
font-family: sans-serif;
margin-top: 5px;
margin-left: 5px;
padding: 5px;
width: 30%;
border: 2px solid black;
font-size: 14px;
color: #222;
background-color: #fff;
border-radius: 3px;
}
Finally, create three new constants that you will use to target the new <span>
s using their IDs. Add the following code to your JavaScript, above the closing </script>
tag:
const magDisplay = document.getElementById('mag');
const locDisplay = document.getElementById('loc');
const dateDisplay = document.getElementById('date');
Save your work and refresh the page. The new sidebar will be on the left side of the page. In an upcoming step, you will use the <span>
s that you created to display the magnitude, location, and time of the earthquakes in the sidebar.
Add the earthquake source
The Mapbox GL JS addSource
instance lets you add a new data source to a map. In this case, you will use data from the USGS Earthquake Catalog API, which returns information about recent earthquakes, including the magnitude, location, and the time at which the earthquake happened.
By default, the Earthquake Catalog API returns all events from the prior 30 days. But for this web app, you will instead return events from the past seven days using the optional starttime
parameter. You will use the JavaScript Date
constructor to get the date seven days ago as an ISO 8601 timestamp, as required by the USGS Earthquake Catalog API. Add the following JavaScript to your file:
const today = new Date();
// Use JavaScript to get the date a week ago
const priorDate = new Date().setDate(today.getDate() - 7);
// Set that to an ISO8601 timestamp as required by the USGS earthquake API
const priorDateTs = new Date(priorDate);
const sevenDaysAgo = priorDateTs.toISOString();
You will use the sevenDaysAgo
constant in a call to the Earthquake Catalog API. (If you use a time-specific JavaScript library like Moment.js, getting the current date minus seven days will take fewer steps. This tutorial shows the implementation in plain JavaScript.)
For this web app, you will use the Earthquake Catalog API's optional format
parameter to return the data as GeoJSON. You will also restrict the results to earthquakes with a magnitude of one or higher by using the optional eventtype
and minmagnitude
parameters.
With these optional parameters, the USGS Earthquake Catalog API query will be:
"https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&eventtype=earthquake&minmagnitude=1&starttime=" + sevenDaysAgo
To add results from a call to the Earthquake Catalog API to your app as a source, you will wrap addSource
inside a map.on('load')
function so that the new source is not loaded before the map is rendered. Add the following code to your JavaScript:
map.on('load', () => {
map.addSource('earthquakes', {
type: 'geojson',
data: `https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&eventtype=earthquake&minmagnitude=1&starttime=${sevenDaysAgo}`,
generateId: true // This ensures that all features have unique IDs
});
});
Feature state relies on each feature having a numeric id
that is unique
across the source or source layer.If you are using a GeoJSON source that is
generated at runtime, as with the results from the Earthquake Catalog API in
this tutorial, you can add an id
to each feature by setting the generateId
property in map.addSource()
to true
. This generates an id
for each
feature in the new source, based on the feature's index in the source data.The
generateId
method only works for GeoJSON sources that are added at runtime.
For tilesets, or for GeoJSON sources that are added before runtime, you must
set unique id
s for each feature before the source is added to the map.
In the next step, you will add a style layer that uses the source earthquakes
to the map.
Add the earthquake layer
The Mapbox GL JS addLayer
instance creates a new map layer, which defines styling for data from a specified source. In this case, the data is coming from the earthquakes
source that you created in the last step.
Add the following addLayer
function inside map.on('load')
, below the addSource
function you wrote in the last step:
map.addLayer({
id: 'earthquakes-viz',
type: 'circle',
source: 'earthquakes',
paint: {
'circle-stroke-color': '#000',
'circle-stroke-width': 1,
'circle-color': '#000'
}
});
Save your work and refresh the page in your browser. You will see that the new earthquake-viz
layer draws each earthquake as a black dot, as specified in the paint
property. In the next step, you will use:
feature-state
to change the style of an individual feature when the user hovers over it
interpolate
,linear
, andget
expressions to determine what that new style should be, based on the magnitude of the earthquake
Style the earthquakes using feature state
Feature state can be used to style any of the paint properties that support data-driven styling with values from these attributes. Learn more about paint property support levels in the Mapbox style specification. In feature state, features are identified by their id
property, which must be unique across a source or source layer. When you used addSource
to add the earthquake data as a source, you set 'generateId': true
, which ensures that all features have unique IDs.
In this case, you will use feature state to change the color and radius size of the circle marking a feature based on an attribute (hover
) with a boolean value. Then you will define the hover
attribute using setFeatureState
.
Update addLayer
to use feature state for circle-radius
and circle-color
. The updated code also uses the expressions interpolate
operator to set the circle-radius
and the circle-color
according to the magnitude of the selected earthquake feature.
map.addLayer({
id: 'earthquakes-viz',
type: 'circle',
source: 'earthquakes',
paint: {
// The feature-state dependent circle-radius expression will render
// the radius size according to its magnitude when
// a feature's hover state is set to true
'circle-radius': [
'case',
['boolean', ['feature-state', 'hover'], false],
[
'interpolate',
['linear'],
['get', 'mag'],
1,
8,
1.5,
10,
2,
12,
2.5,
14,
3,
16,
3.5,
18,
4.5,
20,
6.5,
22,
8.5,
24,
10.5,
26
],
5
],
'circle-stroke-color': '#000',
'circle-stroke-width': 1,
// The feature-state dependent circle-color expression will render
// the color according to its magnitude when
// a feature's hover state is set to true
'circle-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
[
'interpolate',
['linear'],
['get', 'mag'],
1,
'#fff7ec',
1.5,
'#fee8c8',
2,
'#fdd49e',
2.5,
'#fdbb84',
3,
'#fc8d59',
3.5,
'#ef6548',
4.5,
'#d7301f',
6.5,
'#b30000',
8.5,
'#7f0000',
10.5,
'#000'
],
'#000'
]
}
});
Define the hover attribute
Next, you will use setFeatureState
to define the hover
attribute. To trigger the feature state changes when a user mouses over an earthquake feature, you will wrap setFeatureState
in a map.on('mousemove')
function.
The following code creates several new constants, each of which will be updated for every map.on('mousemove')
event:
quakeID
: This constant is set to theid
of the current feature, which allows you to target each earthquake individually.quakeMagnitude
: The returned magnitude of the current feature.quakeLocation
: The returned location of the current feature.quakeDate
: The returned date of the current feature.
Paste the following code into your JavaScript, above the final </script>
tag:
let quakeID = null;
map.on('mousemove', 'earthquakes-viz', (event) => {
map.getCanvas().style.cursor = 'pointer';
// Set constants equal to the current feature's magnitude, location, and time
const quakeMagnitude = event.features[0].properties.mag;
const quakeLocation = event.features[0].properties.place;
const quakeDate = new Date(event.features[0].properties.time);
// Check whether features exist
if (event.features.length === 0) return;
// Display the magnitude, location, and time in the sidebar
magDisplay.textContent = quakeMagnitude;
locDisplay.textContent = quakeLocation;
dateDisplay.textContent = quakeDate;
// If quakeID for the hovered feature is not null,
// use removeFeatureState to reset to the default behavior
if (quakeID) {
map.removeFeatureState({
source: 'earthquakes',
id: quakeID
});
}
quakeID = event.features[0].id;
// When the mouse moves over the earthquakes-viz layer, update the
// feature state for the feature under the mouse
map.setFeatureState(
{
source: 'earthquakes',
id: quakeID
},
{
hover: true
}
);
});
Save your work and refresh your browser page. Now, when you mouse over an earthquake feature, two things will happen:
- The magnitude, location, and date of the earthquake will be displayed in the sidebar.
- The styling of the circle that marks the feature will change based on the returned
mag
property.
When you move your mouse away from an earthquake feature, though, the circle will stay the same size and color, and the sidebar will continue displaying that feature's information, until you mouse over another feature. You will fix this in the next step.
Reset the feature state
The final step in creating this web app is to update the feature state of an earthquake feature when the user mouses away from it. You will also use this map.on('mouseleave')
function to reset the sidebar display when the user mouses away.
Paste the following code into your JavaScript above the closing </script>
tag:
map.on('mouseleave', 'earthquakes-viz', () => {
if (quakeID) {
map.setFeatureState(
{
source: 'earthquakes',
id: quakeID
},
{
hover: false
}
);
}
quakeID = null;
// Remove the information from the previously hovered feature from the sidebar
magDisplay.textContent = '';
locDisplay.textContent = '';
dateDisplay.textContent = '';
// Reset the cursor style
map.getCanvas().style.cursor = '';
});
Save your work, then refresh the page in your browser. Now when you mouse over an earthquake feature and then move the mouse away from it, the sidebar will reset until you mouse over a new earthquake feature.
Final product
You have created a web app that uses feature state to dynamically style earthquake features on a map, based on the earthquake's magnitude.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Demo: Create interactive hover effects with 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 {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
.quake-info {
position: absolute;
font-family: sans-serif;
margin-top: 5px;
margin-left: 5px;
padding: 5px;
width: 30%;
border: 2px solid black;
font-size: 14px;
color: #222;
background-color: #fff;
border-radius: 3px;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="quake-info">
<div><strong>Magnitude:</strong> <span id="mag"></span></div>
<div><strong>Location:</strong> <span id="loc"></span></div>
<div><strong>Date:</strong> <span id="date"></span></div>
</div>
<script>
mapboxgl.accessToken = '{{MAPBOX_ACCESS_TOKEN}}';
const map = new mapboxgl.Map({
container: 'map', // Specify the container ID
style: 'mapbox://styles/mapbox/streets-v12', // Specify which map style to use
center: [-122.44121, 37.76132], // Specify the starting position [lng, lat]
zoom: 3.5 // Specify the starting zoom
});
// We only want to return earthquakes that happened in the last week
// Use JavaScript to get today's date
const today = new Date();
// Use JavaScript to get the date a week ago
const priorDate = new Date().setDate(today.getDate() - 7);
// Set that to an ISO8601 timestamp as required by the USGS earthquake API
const priorDateTs = new Date(priorDate);
const sevenDaysAgo = priorDateTs.toISOString();
// Target the span elements used in the sidebar
const magDisplay = document.getElementById('mag');
const locDisplay = document.getElementById('loc');
const dateDisplay = document.getElementById('date');
map.on('load', () => {
// When the map loads, add the data from the USGS earthquake API as a source
map.addSource('earthquakes', {
'type': 'geojson',
'data': `https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&eventtype=earthquake&minmagnitude=1&starttime=${sevenDaysAgo}`, // Use the sevenDaysAgo variable to only retrieve quakes from the past week
'generateId': true // This ensures that all features have unique IDs
});
// Add earthquakes as a layer and style it
map.addLayer({
'id': 'earthquakes-viz',
'type': 'circle',
'source': 'earthquakes',
'paint': {
// The feature-state dependent circle-radius expression will render
// the radius size according to its magnitude when
// a feature's hover state is set to true
'circle-radius': [
'case',
['boolean', ['feature-state', 'hover'], false],
[
'interpolate',
['linear'],
['get', 'mag'],
1,
8,
1.5,
10,
2,
12,
2.5,
14,
3,
16,
3.5,
18,
4.5,
20,
6.5,
22,
8.5,
24,
10.5,
26
],
5
],
'circle-stroke-color': '#000',
'circle-stroke-width': 1,
// The feature-state dependent circle-color expression will render
// the color according to its magnitude when
// a feature's hover state is set to true
'circle-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
[
'interpolate',
['linear'],
['get', 'mag'],
1,
'#fff7ec',
1.5,
'#fee8c8',
2,
'#fdd49e',
2.5,
'#fdbb84',
3,
'#fc8d59',
3.5,
'#ef6548',
4.5,
'#d7301f',
6.5,
'#b30000',
8.5,
'#7f0000',
10.5,
'#000'
],
'#000'
]
}
});
let quakeID = null;
map.on('mousemove', 'earthquakes-viz', (event) => {
map.getCanvas().style.cursor = 'pointer';
// Set variables equal to the current feature's magnitude, location, and time
const quakeMagnitude = event.features[0].properties.mag;
const quakeLocation = event.features[0].properties.place;
const quakeDate = new Date(event.features[0].properties.time);
if (event.features.length === 0) return;
// Display the magnitude, location, and time in the sidebar
magDisplay.textContent = quakeMagnitude;
locDisplay.textContent = quakeLocation;
dateDisplay.textContent = quakeDate;
// When the mouse moves over the earthquakes-viz layer, update the
// feature state for the feature under the mouse
if (quakeID) {
map.removeFeatureState({
source: 'earthquakes',
id: quakeID
});
}
quakeID = event.features[0].id;
map.setFeatureState(
{
source: 'earthquakes',
id: quakeID
},
{
hover: true
}
);
});
// When the mouse leaves the earthquakes-viz layer, update the
// feature state of the previously hovered feature
map.on('mouseleave', 'earthquakes-viz', () => {
if (quakeID) {
map.setFeatureState(
{
source: 'earthquakes',
id: quakeID
},
{
hover: false
}
);
}
quakeID = null;
// Remove the information from the previously hovered feature from the sidebar
magDisplay.textContent = '';
locDisplay.textContent = '';
dateDisplay.textContent = '';
// Reset the cursor style
map.getCanvas().style.cursor = '';
});
});
</script>
</body>
</html>
Next steps
To learn more about feature state expressions and how you can use them to dynamically style map features, explore the following resources:
- Explore the Mapbox GL JS documentation for
setFeatureState
,removeFeatureState
, andgetFeatureState
. - See how Mapbox used feature state to create a map using data from the 2016 presidential election in the Live Electoral Maps: A Guide to Feature State blog post.
- Learn more about using events and feature state to create a per-feature styling change in the Create a hover effect example.
- Learn more about how to use Mapbox GL JS expressions in the Get started with Mapbox GL JS expressions tutorial.