GeoJSON Overlay
Load and display GeoJSON data with custom styling and popups Each library has its own approach: here's how they compare.
Quick Recommendations
Leaflet — Ideal for interactive GeoJSON overlays where onEachFeature makes it trivial to bind popups and events to each feature.
MapLibre GL JS — The right choice when GeoJSON data should be GPU-rendered as part of the vector tile pipeline with expression-based styling.
OpenLayers — Makes the most sense when GeoJSON features need projection transformation, complex style rules, or integration with OGC data sources.
MapTiler SDK — Gives MapTiler SDK users MapLibre's GeoJSON source/layer model, plus automatic basemap 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 | 18 | 23 | 28 | 26 |
Code Snippets
Click a library to expand its code. Each snippet shows only the core logic: the scaffold handles HTML boilerplate.
Leaflet18 linesShow code
const map = L.map('map').setView([50.0755, 14.4378], 6);
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 geojsonData = {
type: 'FeatureCollection',
features: [
{ type: 'Feature', properties: { name: 'Prague' }, geometry: { type: 'Point', coordinates: [14.4378, 50.0755] } },
{ type: 'Feature', properties: { name: 'Brno' }, geometry: { type: 'Point', coordinates: [16.6068, 49.1951] } },
{ type: 'Feature', properties: { name: 'Ostrava' }, geometry: { type: 'Point', coordinates: [18.2625, 49.8209] } },
{ type: 'Feature', properties: { name: 'Plzen' }, geometry: { type: 'Point', coordinates: [13.3776, 49.7384] } }
]
};
L.geoJSON(geojsonData, {
pointToLayer: (f, latlng) => L.circleMarker(latlng, { radius: 8, fillColor: '#FF5722', color: '#fff', weight: 2, fillOpacity: 0.8 }),
onEachFeature: (f, layer) => layer.bindPopup(f.properties.name)
}).addTo(map);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: [-100, 40],
zoom: 3
});
map.on('load', async () => {
const res = await fetch('https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_1_states_provinces_shp.geojson');
const data = await res.json();
map.addSource('states', { type: 'geojson', data });
const labels = map.getStyle().layers.find(l => l.type === 'symbol' && /label/.test(l.id));
map.addLayer({ id: 'states-fill', type: 'fill', source: 'states',
paint: { 'fill-color': '#6366f1', 'fill-opacity': 0.15 }
}, labels ? labels.id : undefined);
map.addLayer({ id: 'states-border', type: 'line', source: 'states',
paint: { 'line-color': '#6366f1', 'line-width': 1 } });
map.on('click', 'states-fill', (e) => {
const f = e.features[0];
new maplibregl.Popup().setLngLat(e.lngLat).setHTML(`<b>${f.properties.name || 'Unknown'}</b>`).addTo(map);
});
map.on('mouseenter', 'states-fill', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'states-fill', () => { map.getCanvas().style.cursor = ''; });
});OpenLayers28 linesShow code
var geojsonData = {
type: 'FeatureCollection',
features: [
{ type: 'Feature', properties: { name: 'Louvre Museum' }, geometry: { type: 'Point', coordinates: [2.3376, 48.8606] } },
{ type: 'Feature', properties: { name: 'Walking Path' }, geometry: { type: 'LineString', coordinates: [[2.3376, 48.8606], [2.3272, 48.8600], [2.3200, 48.8580]] } },
{ type: 'Feature', properties: { name: 'Park Area' }, geometry: { type: 'Polygon', coordinates: [[[2.3272, 48.8620], [2.3340, 48.8620], [2.3340, 48.8590], [2.3272, 48.8590], [2.3272, 48.8620]]] } }
]
};
var vectorSource = new ol.source.Vector({
features: new ol.format.GeoJSON().readFeatures(geojsonData, { featureProjection: 'EPSG:3857' })
});
var vectorLayer = new ol.layer.Vector({
source: vectorSource,
style: function(feature) {
var type = feature.getGeometry().getType();
if (type === 'Point') return new ol.style.Style({ image: new ol.style.Circle({ radius: 7, fill: new ol.style.Fill({ color: '#e63946' }), stroke: new ol.style.Stroke({ color: '#fff', width: 2 }) }) });
if (type === 'LineString') return new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#457b9d', width: 3 }) });
return new ol.style.Style({ fill: new ol.style.Fill({ color: 'rgba(42, 157, 143, 0.3)' }), stroke: new ol.style.Stroke({ color: '#2a9d8f', width: 2 }) });
}
});
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>' }) }),
vectorLayer
],
view: new ol.View({ center: ol.proj.fromLonLat([2.328, 48.860]), zoom: 15 })
});MapTiler SDK26 linesShow code
maptilersdk.config.apiKey = 'YOUR_MAPTILER_KEY';
const map = new maptilersdk.Map({
container: 'map',
style: maptilersdk.MapStyle.STREETS,
center: [-98, 39],
zoom: 3
});
map.on('load', () => {
map.addSource('earthquakes', {
type: 'geojson',
data: 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/significant_month.geojson'
});
map.addLayer({
id: 'quake-points', type: 'circle', source: 'earthquakes',
paint: {
'circle-radius': 8,
'circle-color': '#9C27B0',
'circle-stroke-width': 2,
'circle-stroke-color': '#e0e0e0'
}
});
map.on('click', 'quake-points', (e) => {
const props = e.features[0].properties;
new maptilersdk.Popup().setLngLat(e.lngLat).setHTML(`<b>${props.title}</b>`).addTo(map);
});
});Things to Know
Implementation-specific details that matter for geojson overlay.
L.geoJSON() auto-detects geometry types and renders them; coordinate order in GeoJSON [lng, lat] is handled automatically.
GeoJSON is loaded as a source, then styled with separate layers per geometry type (fill, line, circle); more verbose but more powerful.
ol.format.GeoJSON reads and writes GeoJSON; features must be transformed to the map projection (EPSG:3857) on import.
Same source/layer model as MapLibre; GeoJSON features are added to a source and displayed via typed layers.
Which Library to Use?
Fetch a GeoJSON file and render its features on a Leaflet map with custom styling and interactive popups.
Ideal for interactive GeoJSON overlays where onEachFeature makes it trivial to bind popups and events to each feature.
Fetch a remote GeoJSON file and add it as a source with styled layers on a MapLibre GL JS map.
The right choice when GeoJSON data should be GPU-rendered as part of the vector tile pipeline with expression-based styling.
Fetch and display GeoJSON features on an OpenLayers map using ol.format.GeoJSON and a styled vector layer.
Makes the most sense when GeoJSON features need projection transformation, complex style rules, or integration with OGC data sources.
Load and display a GeoJSON file as a styled source and layer on a MapTiler SDK map.
Gives MapTiler SDK users MapLibre's GeoJSON source/layer model, plus automatic basemap setup.
Build Maps with AI
Generate code like these examples using mapping library skills in your IDE.