Skip to content

Cluster Markers in Leaflet with Leaflet.markercluster

Leaflet.markercluster groups nearby markers into one numbered circle and splits them up again as you zoom in. This example loads about 7,000 earthquakes onto Maptoolkit raster tiles. Click a cluster to zoom into it, and click a single marker for its magnitude.

const map = L.map("map").setView([40.67, -103.59], 3);

    L.tileLayer("https://rtc-cdn.maptoolkit.net/rtc/maptoolkit-maptoolkit.summer/{z}/{x}/{y}{r}.png?api_key=YOUR_API_KEY", {
      maxZoom: 18,
      attribution:
        "© <a href='https://www.maptoolkit.com' target='_blank'>Maptoolkit</a> " +
        "© <a href='https://www.openstreetmap.org/copyright' target='_blank'>OSM</a>",
    }).addTo(map);

    // One cluster group holds every marker and decides what to show at each zoom.
    const clusters = L.markerClusterGroup({ chunkedLoading: true });

    fetch("https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson")
      .then((response) => response.json())
      .then((data) => {
        const markers = L.geoJSON(data, {
          onEachFeature: (feature, layer) => {
            layer.bindPopup("Magnitude " + feature.properties.mag);
          },
        });
        clusters.addLayers(markers.getLayers());
        map.addLayer(clusters);
      });
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css" />
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" />
  <script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
  <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 map = L.map("map").setView([40.67, -103.59], 3);

    L.tileLayer("https://rtc-cdn.maptoolkit.net/rtc/maptoolkit-maptoolkit.summer/{z}/{x}/{y}{r}.png?api_key=YOUR_API_KEY", {
      maxZoom: 18,
      attribution:
        "© <a href='https://www.maptoolkit.com' target='_blank'>Maptoolkit</a> " +
        "© <a href='https://www.openstreetmap.org/copyright' target='_blank'>OSM</a>",
    }).addTo(map);

    // One cluster group holds every marker and decides what to show at each zoom.
    const clusters = L.markerClusterGroup({ chunkedLoading: true });

    fetch("https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson")
      .then((response) => response.json())
      .then((data) => {
        const markers = L.geoJSON(data, {
          onEachFeature: (feature, layer) => {
            layer.bindPopup("Magnitude " + feature.properties.mag);
          },
        });
        clusters.addLayers(markers.getLayers());
        map.addLayer(clusters);
      });
  </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.

Use the Maptoolkit Connector. Create a Leaflet map with Maptoolkit raster tiles at zoom level 3, centered on [40.67, -103.59]. Load the earthquakes GeoJSON from https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson into a Leaflet.markercluster group, with a popup showing each earthquake’s magnitude.

How it works

L.markerClusterGroup is a single layer that holds every marker. At each zoom level it groups markers that are close together on screen and draws one circle with the count in place of the group. Zooming in splits the groups, and at the maximum zoom, markers that still overlap fan out when clicked so each one can be reached.

Both stylesheets are required. MarkerCluster.css handles the animations, and MarkerCluster.Default.css draws the colored circles. Without the second one, the clusters render as bare numbers with no background.

L.geoJSON turns each feature into a marker and converts the GeoJSON [longitude, latitude] order into Leaflet’s [latitude, longitude] for you. onEachFeature binds the popup while the feature’s properties are still at hand.

addLayers() with an array is the fast path. It adds all markers in one batch, and chunkedLoading: true spreads that work over several frames so the page stays responsive while thousands of markers are processed. Calling addLayer() once per marker in a loop is much slower at this size.

Raster tiles need an attribution option on the tile layer, because a tile image carries no credit of its own.

Next steps

The circles show a count by default. iconCreateFunction in the cluster options lets you build the icon yourself from cluster.getAllChildMarkers(), so a cluster can show the sum of a value, such as total capacity, or take its color from the worst magnitude inside it.

Every marker in Leaflet is a DOM element, and the plugin keeps the page fast by only showing what is on screen. Past tens of thousands of points, clustering on the GPU scales further: Create and Style Clusters does the same with the same data in Maptoolkit Maps JS, and the clusters are computed inside the map’s data source.