Skip to content
Documentation menu
docs / examples / hover-highlight
Example

Hover highlight with feature-state

Highlight the polygon under the pointer without touching the data. feature-state flips a flag on one feature and the paint expression reads it, so hover costs no setData and no re-tiling, even on large sources.

ts
import { createMap } from "@mapmap/maps";

const map = createMap({
  container: "map",
  apiKey: "snk_...",
  attributionPosition: "bottom-left",
  center: [-0.12, 51.5],
  zoom: 10.3,
});
await map.whenReady();

const cells: GeoJSON.Feature[] = [];
for (let x = 0; x < 18; x++) {
  for (let y = 0; y < 12; y++) {
    const west = -0.42 + x * 0.034, south = 51.36 + y * 0.025;
    cells.push({
      type: "Feature",
      properties: { cell: x * 100 + y },
      geometry: {
        type: "Polygon",
        coordinates: [[
          [west, south], [west + 0.034, south], [west + 0.034, south + 0.025],
          [west, south + 0.025], [west, south],
        ]],
      },
    });
  }
}

map.map.addSource("areas", {
  type: "geojson",
  data: { type: "FeatureCollection", features: cells },
  // feature-state needs a stable id per feature: promote one of yours.
  promoteId: "cell",
});

// Slots arrive in the SDK 0.17: map.slot("middle") replaces "road-labels".
map.map.addLayer({
  id: "areas-fill",
  type: "fill",
  source: "areas",
  paint: {
    "fill-color": "#1a6bff",
    "fill-opacity": [
      "case", ["boolean", ["feature-state", "hover"], false], 0.55, 0.12,
    ],
  },
}, "road-labels");

map.map.addLayer({
  id: "areas-outline",
  type: "line",
  source: "areas",
  paint: { "line-color": "#1a6bff", "line-width": 0.6 },
}, "road-labels");

let hovered: string | number | undefined;
const setHover = (id: string | number | undefined) => {
  if (hovered !== undefined) {
    map.map.setFeatureState({ source: "areas", id: hovered }, { hover: false });
  }
  hovered = id;
  if (id !== undefined) {
    map.map.setFeatureState({ source: "areas", id }, { hover: true });
  }
};

map.map.on("mousemove", "areas-fill", (e) => {
  setHover(e.features?.[0]?.id);
  map.map.getCanvas().style.cursor = "pointer";
});
map.map.on("mouseleave", "areas-fill", () => {
  setHover(undefined);
  map.map.getCanvas().style.cursor = "";
});

Worth knowing

  • feature-state is only readable from paint properties, never from layout or filter. Use it for colour, opacity and width; use a filter or setData for anything that changes which features draw.
  • Ids must be stable. On a GeoJSON source use promoteId (a property) or feature.id; generateId: true works but renumbers on every setData. On a vector source pass sourceLayer to setFeatureState, and the id must be the same on every tile the feature crosses.
  • MapMap basemap features carry tile-assigned ids, not OpenStreetMap ids, so hover on basemap layers highlights per tile and an id never matches an OSM way.