Marker Clustering
Group nearby markers into cluster badges Each library has its own approach: here's how they compare.
Quick Recommendations
Leaflet — A strong fit when you need mature, well-established clustering with animated expand/collapse and extensive customization via the MarkerCluster plugin.
MapLibre GL JS — Best when clustering is part of a larger data-driven visualization using expressions to style cluster circles and counts.
OpenLayers — Ideal when you need distance-based clustering integrated with OpenLayers' vector source pipeline and style functions.
MapTiler SDK — Gives MapTiler SDK users MapLibre's built-in clustering, plus simpler API key setup.
At a Glance
| Leaflet | MapLibre | OpenLayers | SDK | |
|---|---|---|---|---|
| Coordinates | [lat, lng] | [lng, lat] | fromLonLat([lng, lat]) | [lng, lat] |
| Rendering | DOM + Canvas (2D) | WebGL (GPU, 60fps) | Canvas + WebGL | WebGL (GPU, 60fps) |
| Bundle size | ~42 KB gzipped | ~250 KB gzipped | ~290 KB gzipped (tree-shakeable) | ~280 KB gzipped (includes MapLibre) |
| Learning curve | Easiest (simple API, great tutorials) | Moderate (source/layer model, expressions) | Steepest (projections, OGC standards, verbose API) | Easy (MapLibre API with convenience helpers) |
| 3D support | No native 3D (2D only) | Full: terrain, fill-extrusion, globe, sky | Limited: WebGL layers, no native globe | Full (same as MapLibre) |
| Code lines | 14 | 23 | 28 | 34 |
Implementation Details
| Leaflet | MapLibre | OpenLayers | SDK | |
|---|---|---|---|---|
| Implementation | Plugin: leaflet.markercluster | Built-in: GeoJSON source cluster option | Built-in: ol.source.Cluster | Built-in: GeoJSON source cluster (same as MapLibre) |
Code Snippets
Click a library to expand its code. Each snippet shows only the core logic: the scaffold handles HTML boilerplate.
Leaflet14 linesShow code
const map = L.map('map').setView([48.8566, 2.3522], 12);
L.tileLayer('https://api.maptiler.com/maps/streets-v4/256/{z}/{x}/{y}.png?key=YOUR_MAPTILER_KEY', {
tileSize: 256,
attribution: '© <a href="https://www.maptiler.com/">MapTiler</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>'
}).addTo(map);
const markers = L.markerClusterGroup();
const points = [
[48.8584, 2.2945], [48.8606, 2.3376], [48.8530, 2.3499],
[48.8738, 2.2950], [48.8462, 2.3464], [48.8620, 2.3522],
[48.8490, 2.3080], [48.8650, 2.3210], [48.8430, 2.3680],
[48.8710, 2.3150], [48.8550, 2.2830], [48.8670, 2.3600]
];
points.forEach(p => markers.addLayer(L.marker(p)));
map.addLayer(markers);MapLibre GL JS23 linesShow code
const map = new maplibregl.Map({
container: 'map',
style: 'https://api.maptiler.com/maps/streets-v4/style.json?key=YOUR_MAPTILER_KEY',
center: [15.5, 49.8],
zoom: 6
});
const pts = Array.from({ length: 300 }, () => ({
type: 'Feature',
geometry: { type: 'Point', coordinates: [12 + Math.random() * 7, 48.5 + Math.random() * 2.5] }
}));
map.on('load', () => {
map.addSource('pts', { type: 'geojson', data: { type: 'FeatureCollection', features: pts }, cluster: true, clusterMaxZoom: 14, clusterRadius: 50 });
map.addLayer({ id: 'clusters', type: 'circle', source: 'pts', filter: ['has', 'point_count'],
paint: { 'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 50, '#f1f075', 200, '#f28cb1'], 'circle-radius': ['step', ['get', 'point_count'], 18, 50, 25, 200, 35] } });
map.addLayer({ id: 'cluster-count', type: 'symbol', source: 'pts', filter: ['has', 'point_count'],
layout: { 'text-field': '{point_count_abbreviated}', 'text-font': ['Noto Sans Regular'], 'text-size': 12 } });
map.addLayer({ id: 'single', type: 'circle', source: 'pts', filter: ['!', ['has', 'point_count']],
paint: { 'circle-color': '#11b4da', 'circle-radius': 6, 'circle-stroke-width': 1, 'circle-stroke-color': '#fff' } });
map.on('click', 'clusters', (e) => {
const cid = e.features[0].properties.cluster_id;
map.getSource('pts').getClusterExpansionZoom(cid, (err, z) => { if (!err) map.easeTo({ center: e.features[0].geometry.coordinates, zoom: z }); });
});
});OpenLayers28 linesShow code
var features = [];
for (var i = 0; i < 200; i++) {
features.push(new ol.Feature({
geometry: new ol.geom.Point(ol.proj.fromLonLat([2.3 + Math.random() * 0.3, 48.8 + Math.random() * 0.15]))
}));
}
var clusterSource = new ol.source.Cluster({
distance: 40,
source: new ol.source.Vector({ features: features })
});
var clusterLayer = new ol.layer.Vector({
source: clusterSource,
style: function(feature) {
var size = feature.get('features').length;
return new ol.style.Style({
image: new ol.style.Circle({ radius: 10 + Math.min(size, 20), fill: new ol.style.Fill({ color: size > 5 ? '#e63946' : '#457b9d' }), stroke: new ol.style.Stroke({ color: '#fff', width: 2 }) }),
text: new ol.style.Text({ text: size.toString(), fill: new ol.style.Fill({ color: '#fff' }), font: 'bold 12px sans-serif' })
});
}
});
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({ source: new ol.source.XYZ({ url: 'https://api.maptiler.com/maps/streets-v4/256/{z}/{x}/{y}.png?key=YOUR_MAPTILER_KEY', attributions: '© <a href="https://www.maptiler.com/copyright/">MapTiler</a>' }) }),
clusterLayer
],
view: new ol.View({ center: ol.proj.fromLonLat([2.45, 48.87]), zoom: 11 })
});MapTiler SDK34 linesShow code
maptilersdk.config.apiKey = 'YOUR_MAPTILER_KEY';
const map = new maptilersdk.Map({
container: 'map',
style: maptilersdk.MapStyle.STREETS,
center: [2.3522, 48.8566],
zoom: 11
});
const points = [];
for (let i = 0; i < 80; i++) {
points.push({
type: 'Feature',
geometry: { type: 'Point', coordinates: [2.25 + Math.random() * 0.25, 48.8 + Math.random() * 0.15] },
properties: { name: 'Location ' + (i + 1) }
});
}
map.on('load', () => {
map.addSource('points', {
type: 'geojson',
data: { type: 'FeatureCollection', features: points },
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
map.addLayer({ id: 'clusters', type: 'circle', source: 'points', filter: ['has', 'point_count'],
paint: { 'circle-color': '#9C27B0', 'circle-radius': ['step', ['get', 'point_count'], 15, 10, 22, 30, 30], 'circle-stroke-width': 2, 'circle-stroke-color': '#ffffff' }
});
map.addLayer({ id: 'cluster-count', type: 'symbol', source: 'points', filter: ['has', 'point_count'],
layout: { 'text-field': '{point_count_abbreviated}', 'text-size': 13 },
paint: { 'text-color': '#ffffff' }
});
map.addLayer({ id: 'unclustered', type: 'circle', source: 'points', filter: ['!', ['has', 'point_count']],
paint: { 'circle-color': '#ce93d8', 'circle-radius': 6, 'circle-stroke-width': 1, 'circle-stroke-color': '#ffffff' }
});
});Things to Know
Implementation-specific details that matter for marker clustering.
Requires the external leaflet.markercluster plugin (~35 KB); provides spiderfying and animated transitions out of the box.
Clustering is a GeoJSON source option: cluster count and radius are configured declaratively, and cluster styling uses expressions.
ol.source.Cluster wraps another vector source; you control cluster distance and style via a style function that reads the features array.
Identical clustering behavior to MapLibre since it uses the same GeoJSON source cluster engine under the hood.
Which Library to Use?
Group nearby markers into expandable clusters using the Leaflet.markercluster plugin for better performance with large datasets.
A strong fit when you need mature, well-established clustering with animated expand/collapse and extensive customization via the MarkerCluster plugin.
Group nearby points into clusters using the built-in GeoJSON source clustering in MapLibre GL JS -- no plugins needed.
Best when clustering is part of a larger data-driven visualization using expressions to style cluster circles and counts.
Group nearby point features into clusters using ol.source.Cluster with dynamic styling based on cluster size.
Ideal when you need distance-based clustering integrated with OpenLayers' vector source pipeline and style functions.
Cluster dense point data using a GeoJSON source with built-in clustering support in MapTiler SDK.
Gives MapTiler SDK users MapLibre's built-in clustering, plus simpler API key setup.
Build Maps with AI
Generate code like these examples using mapping library skills in your IDE.