MapSnippets logoMapSnippets logo

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

LeafletMapLibreOpenLayersSDK
Coordinates[lat, lng][lng, lat]fromLonLat([lng, lat])[lng, lat]
RenderingDOM + Canvas (2D)WebGL (GPU, 60fps)Canvas + WebGLWebGL (GPU, 60fps)
Bundle size~42 KB gzipped~250 KB gzipped~290 KB gzipped (tree-shakeable)~280 KB gzipped (includes MapLibre)
Learning curveEasiest (simple API, great tutorials)Moderate (source/layer model, expressions)Steepest (projections, OGC standards, verbose API)Easy (MapLibre API with convenience helpers)
3D supportNo native 3D (2D only)Full: terrain, fill-extrusion, globe, skyLimited: WebGL layers, no native globeFull (same as MapLibre)
Code lines14232834

Implementation Details

LeafletMapLibreOpenLayersSDK
ImplementationPlugin: leaflet.markerclusterBuilt-in: GeoJSON source cluster optionBuilt-in: ol.source.ClusterBuilt-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
arrow_forwardFull example
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: '&copy; <a href="https://www.maptiler.com/">MapTiler</a> &copy; <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
arrow_forwardFull example
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
arrow_forwardFull example
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: '&copy; <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
arrow_forwardFull example
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.

Leaflet

Requires the external leaflet.markercluster plugin (~35 KB); provides spiderfying and animated transitions out of the box.

MapLibre GL JS

Clustering is a GeoJSON source option: cluster count and radius are configured declaratively, and cluster styling uses expressions.

OpenLayers

ol.source.Cluster wraps another vector source; you control cluster distance and style via a style function that reads the features array.

MapTiler SDK

Identical clustering behavior to MapLibre since it uses the same GeoJSON source cluster engine under the hood.

Which Library to Use?

auto_awesome

Build Maps with AI

Generate code like these examples using mapping library skills in your IDE.