Skip to content

Create a Hover Effect

Highlight GeoJSON features when the user hovers over them using setFeatureState.

🖱️ Hover over the circles to see the highlight effect.

How It Works

  1. Add a GeoJSON source with features that have id fields
  2. Use feature-state expressions in layer paint to change appearance
  3. Use setFeatureState on mouseenter/mouseleave to toggle the state

Key Code Pattern

javascript
// Layer uses feature-state to change color/size on hover
map.addLayer({
  id: 'my-layer',
  type: 'circle',
  source: 'my-source',
  paint: {
    // Changes based on hover state
    'circle-color': ['case',
      ['boolean', ['feature-state', 'hover'], false],
      '#ef4444', // color when hovered
      '#3b82f6'  // default color
    ],
    'circle-radius': ['case',
      ['boolean', ['feature-state', 'hover'], false],
      16, // radius when hovered
      10  // default radius
    ]
  }
});

let hoveredId = null;

map.on('mouseenter', 'my-layer', (e) => {
  map.getCanvas().style.cursor = 'pointer';
  if (e.features.length > 0) {
    if (hoveredId !== null) {
      map.setFeatureState({ source: 'my-source', id: hoveredId }, { hover: false });
    }
    hoveredId = e.features[0].id;
    map.setFeatureState({ source: 'my-source', id: hoveredId }, { hover: true });
  }
});

map.on('mouseleave', 'my-layer', () => {
  map.getCanvas().style.cursor = '';
  if (hoveredId !== null) {
    map.setFeatureState({ source: 'my-source', id: hoveredId }, { hover: false });
  }
  hoveredId = null;
});

Important: Features Need IDs

javascript
// Each feature must have an 'id' field for setFeatureState to work
const geojson = {
  type: 'FeatureCollection',
  features: [
    { type: 'Feature', id: 1, properties: { name: 'Paris' }, geometry: {...} },
    { type: 'Feature', id: 2, properties: { name: 'London' }, geometry: {...} },
  ]
};

Complete Example

html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link href="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.css" rel="stylesheet" />
    <script src="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.js"></script>
    <style>
      #map { height: 500px; width: 100%; }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script>
      const map = new mapmetricsgl.Map({
        container: 'map',
        style: '<StyleFile_URL_with_Token>',
        center: [2.349902, 48.852966],
        zoom: 5
      });

      map.addControl(new mapmetricsgl.NavigationControl(), 'top-right');

      map.on('load', () => {
        map.addSource('cities', {
          type: 'geojson',
          data: {
            type: 'FeatureCollection',
            features: [
              { type: 'Feature', id: 1, properties: { name: 'Paris' },  geometry: { type: 'Point', coordinates: [2.349902, 48.852966] } },
              { type: 'Feature', id: 2, properties: { name: 'London' }, geometry: { type: 'Point', coordinates: [-0.1276, 51.5074] } },
              { type: 'Feature', id: 3, properties: { name: 'Berlin' }, geometry: { type: 'Point', coordinates: [13.405, 52.52] } },
            ]
          }
        });

        map.addLayer({
          id: 'cities-layer',
          type: 'circle',
          source: 'cities',
          paint: {
            'circle-radius': ['case', ['boolean', ['feature-state', 'hover'], false], 16, 10],
            'circle-color':  ['case', ['boolean', ['feature-state', 'hover'], false], '#ef4444', '#3b82f6'],
            'circle-stroke-width': 2,
            'circle-stroke-color': '#fff'
          }
        });

        let hoveredId = null;

        map.on('mouseenter', 'cities-layer', (e) => {
          map.getCanvas().style.cursor = 'pointer';
          if (e.features.length > 0) {
            if (hoveredId !== null) map.setFeatureState({ source: 'cities', id: hoveredId }, { hover: false });
            hoveredId = e.features[0].id;
            map.setFeatureState({ source: 'cities', id: hoveredId }, { hover: true });
          }
        });

        map.on('mouseleave', 'cities-layer', () => {
          map.getCanvas().style.cursor = '';
          if (hoveredId !== null) map.setFeatureState({ source: 'cities', id: hoveredId }, { hover: false });
          hoveredId = null;
        });
      });
    </script>
  </body>
</html>
jsx
import React, { useEffect, useRef } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

const geojsonData = {
  type: 'FeatureCollection',
  features: [
    { type: 'Feature', id: 1, properties: { name: 'Paris' },  geometry: { type: 'Point', coordinates: [2.349902, 48.852966] } },
    { type: 'Feature', id: 2, properties: { name: 'London' }, geometry: { type: 'Point', coordinates: [-0.1276, 51.5074] } },
    { type: 'Feature', id: 3, properties: { name: 'Berlin' }, geometry: { type: 'Point', coordinates: [13.405, 52.52] } },
  ]
};

const HoverEffect = () => {
  const mapContainer = useRef(null);
  const map = useRef(null);

  useEffect(() => {
    if (map.current) return;

    map.current = new mapmetricsgl.Map({
      container: mapContainer.current,
      style: '<StyleFile_URL_with_Token>',
      center: [2.349902, 48.852966],
      zoom: 5
    });

    map.current.addControl(new mapmetricsgl.NavigationControl(), 'top-right');

    map.current.on('load', () => {
      map.current.addSource('cities', { type: 'geojson', data: geojsonData });

      map.current.addLayer({
        id: 'cities-layer',
        type: 'circle',
        source: 'cities',
        paint: {
          'circle-radius': ['case', ['boolean', ['feature-state', 'hover'], false], 16, 10],
          'circle-color':  ['case', ['boolean', ['feature-state', 'hover'], false], '#ef4444', '#3b82f6'],
          'circle-stroke-width': 2,
          'circle-stroke-color': '#fff'
        }
      });

      let hoveredId = null;

      map.current.on('mouseenter', 'cities-layer', (e) => {
        map.current.getCanvas().style.cursor = 'pointer';
        if (e.features.length > 0) {
          if (hoveredId !== null) map.current.setFeatureState({ source: 'cities', id: hoveredId }, { hover: false });
          hoveredId = e.features[0].id;
          map.current.setFeatureState({ source: 'cities', id: hoveredId }, { hover: true });
        }
      });

      map.current.on('mouseleave', 'cities-layer', () => {
        map.current.getCanvas().style.cursor = '';
        if (hoveredId !== null) map.current.setFeatureState({ source: 'cities', id: hoveredId }, { hover: false });
        hoveredId = null;
      });
    });

    return () => { map.current?.remove(); map.current = null; };
  }, []);

  return <div ref={mapContainer} style={{ height: '500px', width: '100%' }} />;
};

export default HoverEffect;

For more information, visit the MapMetrics GitHub repository.