Skip to main content

Create interactive hover effects with Mapbox GL JS

Prerequisite
Familiarity with front-end development concepts.

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>&nbsp;<span id="mag"></span></div>
<div><strong>Location:</strong>&nbsp;<span id="loc"></span></div>
<div><strong>Date:</strong>&nbsp;<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
});
});
Setting unique IDs for features

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 ids 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, and get 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 the id 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>&nbsp;<span id="mag"></span></div>
<div><strong>Location:</strong>&nbsp;<span id="loc"></span></div>
<div><strong>Date:</strong>&nbsp;<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:

Was this page helpful?