Create a Hover Effect in Maptoolkit Maps JS
Feature state in Maptoolkit Maps JS lets you attach arbitrary key-value data to individual features and reference that data in paint expressions, without modifying the source GeoJSON. This example sets a hover boolean on the feature under the cursor and uses it in a fill-color expression to change the layer color. Use feature state for hover highlights, selection states, or any interaction that needs to change the appearance of specific features at runtime.
const API_KEY = 'YOUR_API_KEY';
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [-100.486052, 37.830348],
zoom: 2,
attributionControl: { compact: false }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
let hoveredStateId = null;
map.on('load', () => {
map.addSource('states', {
type: 'geojson',
data: 'https://maplibre.org/maplibre-gl-js/docs/assets/us_states.geojson'
});
map.addLayer({
id: 'state-fills',
type: 'fill',
source: 'states',
paint: {
'fill-color': '#627BC1',
'fill-opacity': [
'case',
['boolean', ['feature-state', 'hover'], false],
1,
0.5
]
}
});
map.addLayer({
id: 'state-borders',
type: 'line',
source: 'states',
paint: {
'line-color': '#627BC1',
'line-width': 2
}
});
map.on('mousemove', 'state-fills', (e) => {
if (e.features.length > 0) {
if (hoveredStateId !== null) {
map.setFeatureState({ source: 'states', id: hoveredStateId }, { hover: false });
}
hoveredStateId = e.features[0].id;
map.setFeatureState({ source: 'states', id: hoveredStateId }, { hover: true });
}
});
map.on('mouseleave', 'state-fills', () => {
if (hoveredStateId !== null) {
map.setFeatureState({ source: 'states', id: hoveredStateId }, { hover: false });
}
hoveredStateId = null;
});
});<!DOCTYPE html>
<html lang="en">
<head>
<title>Create a Hover Effect - Maptoolkit Maps JS</title>
<meta property="og:description" content="Highlight a feature when the user hovers over it using feature state." />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.css" />
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
#map { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
const API_KEY = 'YOUR_API_KEY';
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [-100.486052, 37.830348],
zoom: 2,
attributionControl: { compact: false }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
let hoveredStateId = null;
map.on('load', () => {
map.addSource('states', {
type: 'geojson',
data: 'https://maplibre.org/maplibre-gl-js/docs/assets/us_states.geojson'
});
map.addLayer({
id: 'state-fills',
type: 'fill',
source: 'states',
paint: {
'fill-color': '#627BC1',
'fill-opacity': [
'case',
['boolean', ['feature-state', 'hover'], false],
1,
0.5
]
}
});
map.addLayer({
id: 'state-borders',
type: 'line',
source: 'states',
paint: {
'line-color': '#627BC1',
'line-width': 2
}
});
map.on('mousemove', 'state-fills', (e) => {
if (e.features.length > 0) {
if (hoveredStateId !== null) {
map.setFeatureState({ source: 'states', id: hoveredStateId }, { hover: false });
}
hoveredStateId = e.features[0].id;
map.setFeatureState({ source: 'states', id: hoveredStateId }, { hover: true });
}
});
map.on('mouseleave', 'state-fills', () => {
if (hoveredStateId !== null) {
map.setFeatureState({ source: 'states', id: hoveredStateId }, { hover: false });
}
hoveredStateId = null;
});
});
</script>
</body>
</html>Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.
How it works
Feature state is the technique here, and it is not the same as changing the data.
setFeatureState attaches a value to one feature by id, and a paint expression reads it back
with ['feature-state', 'hover']. Nothing in the source changes, so nothing is re-parsed or
re-uploaded, which is what makes this smooth enough to run on mousemove.
The alternative, rewriting the GeoJSON with a hovered property and calling setData,
re-uploads the whole source on every pointer move.
hoveredStateId tracks which feature is currently lit so the previous one can be cleared.
Skip that and every polygon you pass over stays highlighted.
Feature state needs stable feature ids. GeoJSON without ids needs generateId: true on the
source, or every setFeatureState call silently targets nothing.
mouseleave has to clear the state as well, otherwise the last feature stays highlighted
after the cursor leaves the map entirely.
Next steps
Feature state is not only for hover. A selected state, a visited state, a state driven by something happening elsewhere on the page all work the same way, and combining hover with selection is the usual next step.
Because the paint expression reads the state, you can drive more than colour from it: width, opacity, or a halo that appears only for the active feature. Showing the feature’s data alongside the highlight is what makes it useful rather than decorative.