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.
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.