GeoJSON is the universal language for web maps. If you’re building interactive maps with JavaScript, understanding GeoJSON isn’t optional; it’s essential. This format bridges the gap between your data and the map on the screen, describing both where things are and what they represent. This guide cuts through the theory and gives you the practical knowledge to load, style, and interact with GeoJSON data across the most popular mapping libraries.
You’ll learn the core structure of GeoJSON objects, from simple Points to complex FeatureCollections. We’ll compare how to display your data in Leaflet, Mapbox GL JS (and its open-source sibling MapLibre GL JS), and OpenLayers, helping you choose the right tool. Finally, we’ll move beyond basic display to create interactive choropleth maps, handle large datasets, and integrate maps into modern React applications. Let’s build.
What is GeoJSON? Your Map’s Universal Language
GeoJSON is a standardized format (RFC 7946) for encoding geographic data structures using JSON. Think of it as JSON for maps. It’s human-readable, works natively with JavaScript, and is supported by virtually every web mapping library and API. Its primary role is to act as a lingua franca, seamlessly transferring data from servers, APIs, or databases to your mapping library of choice. At its heart, GeoJSON answers two questions: the geometry (where is it?) and the properties (what is it?).
The GeoJSON Building Blocks: Geometry Types Explained
Every GeoJSON object starts with a geometry. These are the basic shapes you can draw on a map. The coordinate order is critical and a common source of errors: GeoJSON uses [longitude, latitude], which matches the mathematical [x, y] convention, not the LatLng order used by some APIs.
Point: Represents a single location. Use it for markers like a store, a city, or an event.
LineString: An ordered series of points connected by straight lines. Perfect for paths, routes, or rivers.
Polygon: Defines an area. The first and last coordinates must be identical to close the shape. Use it for boundaries of countries, lakes, or property lots.
{
"type": "Point",
"coordinates": [ -122.4194, 37.7749 ] // [longitude, latitude] for San Francisco
}
{
"type": "LineString",
"coordinates": [
[ -122.483, 37.833 ],
[ -122.484, 37.834 ],
[ -122.485, 37.835 ]
]
}
{
"type": "Polygon",
"coordinates": [
[
[ -122.47, 37.80 ], // Outer ring
[ -122.46, 37.80 ],
[ -122.46, 37.78 ],
[ -122.47, 37.78 ],
[ -122.47, 37.80 ] // Must match first coordinate
]
]
}
For more complex data, GeoJSON provides MultiPoint, MultiLineString, and MultiPolygon types to group multiple geometries of the same kind. A GeometryCollection can mix different geometry types in a single object, though it’s less commonly used.
From Geometry to Feature: Adding Meaning with Properties
A raw geometry tells you a shape’s location, but not its meaning. A Feature object wraps a geometry with a properties object, a key-value store for any associated data. This is how you attach a city’s name, population, or a store’s status to a point on the map.
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [ -122.4194, 37.7749 ]
},
"properties": {
"name": "San Francisco",
"population": 881549,
"country": "USA"
}
}
In practice, you rarely work with a single Feature. Data from APIs usually comes as a FeatureCollection, which is simply an array of Features. This is the most common GeoJSON type you’ll load into a map.
{
"type": "FeatureCollection",
"features": [
// ... an array of Feature objects goes here
]
}
Displaying GeoJSON: Choosing Your JavaScript Library
You have GeoJSON data. Now, which library should you use to show it? The choice depends on your project’s needs: simplicity, advanced styling, performance, or complex GIS functionality. The three main contenders are Leaflet, the Mapbox GL JS/MapLibre GL JS duo, and OpenLayers.
Quick recommendation: Choose Leaflet for simplicity and quick wins. Choose Mapbox GL JS or MapLibre GL JS for advanced, performance-sensitive vector maps with dynamic styling. Choose OpenLayers for complex GIS applications that need support for multiple projections and advanced data sources.
Leaflet: The Simple & Powerful Workhorse
Leaflet is the Swiss Army knife for web maps. It’s lightweight, intuitive, and has a massive plugin ecosystem. Adding GeoJSON is straightforward with the L.geoJSON() function. It’s an excellent choice for most interactive maps, especially when you need to get something working fast.
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
</head>
<body>
<div id="map" style="height: 500px;"></div>
<script>
const map = L.map('map').setView([51.505, -0.09], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
const geojsonData = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "name": "London" },
"geometry": { "type": "Point", "coordinates": [ -0.09, 51.505 ] }
}
]
};
L.geoJSON(geojsonData, {
onEachFeature: function (feature, layer) {
layer.bindPopup(`<b>${feature.properties.name}</b>`);
}
}).addTo(map);
</script>
</body>
</html>
This example creates a map, adds OpenStreetMap tiles, and plots a GeoJSON point. The onEachFeature option automatically binds a popup using the feature’s properties. For more complex needs like marker clustering or custom icons, Leaflet’s plugin system has you covered.
Mapbox GL JS & MapLibre GL JS: Vector Power and Advanced Styling
Mapbox GL JS and its open-source fork, MapLibre GL JS, represent a different paradigm. Instead of rendering raster image tiles, they render vector tiles directly in the browser using WebGL. This allows for incredibly dynamic styling: you can change colors, widths, and even 3D extrusion of GeoJSON features based on their properties in real time, without reloading the map.
The key difference: Mapbox GL JS is a commercial product with a polished API and ecosystem, requiring an access token for its styles and services. MapLibre GL JS is a community-driven, open-source fork you can use without restrictions. The APIs are nearly identical.
<!DOCTYPE html>
<html>
<head>
<script src='https://unpkg.com/maplibre-gl@4.0.0/dist/maplibre-gl.js'></script>
<link href='https://unpkg.com/maplibre-gl@4.0.0/dist/maplibre-gl.css' rel='stylesheet' />
</head>
<body>
<div id="map" style="height: 500px;"></div>
<script>
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json', // Open style
center: [-122.4194, 37.7749],
zoom: 10
});
map.on('load', () => {
map.addSource('my-data', {
'type': 'geojson',
'data': {
'type': 'FeatureCollection',
'features': [/* Your GeoJSON features here */]
}
});
map.addLayer({
'id': 'my-layer',
'type': 'fill',
'source': 'my-data',
'paint': {
'fill-color': '#0080ff', // Solid color
'fill-opacity': 0.5
}
});
});
</script>
</body>
</html>
This approach is more powerful but also more complex. You add data as a “source” and then create “layers” that reference that source and define visual style with paint properties. For a deeper dive into building with this ecosystem, see our complete Mapbox GL JS tutorial.
OpenLayers: The GIS Toolkit for Complex Applications
OpenLayers is the powerhouse for GIS-heavy web applications. It has a steeper learning curve than Leaflet but offers unparalleled capabilities out of the box: support for dozens of map projections, advanced vector and raster data sources, complex interactions, and detailed formatting. If your project involves scientific data, historical maps, or requires precise coordinate system transformations, OpenLayers is often the best choice.
import { Map, View } from 'ol';
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer';
import { OSM } from 'ol/source';
import { Vector as VectorSource } from 'ol/source';
import { GeoJSON } from 'ol/format';
// Create a vector source from GeoJSON
const vectorSource = new VectorSource({
format: new GeoJSON(),
url: './data/my-geojson-file.geojson' // Or use a JS object
});
// Create a vector layer
const vectorLayer = new VectorLayer({
source: vectorSource
});
// Create the map
const map = new Map({
target: 'map',
layers: [
new TileLayer({ source: new OSM() }), // Base map
vectorLayer // GeoJSON layer
],
view: new View({ center: [0, 0], zoom: 2 })
});
OpenLayers uses a more formal, object-oriented API. It’s incredibly powerful but may be overkill for a simple map with a few markers. For a broader comparison of these tools, our guide on the best JavaScript map libraries breaks down the pros and cons of each.
Beyond the Basics: Making Your GeoJSON Map Interactive & Useful
Displaying data is just the start. The real value comes from interaction and visualization. Let’s customize markers, create a choropleth map, and add lightweight spatial analysis.
Custom Markers, Popups, and Interactive Styles
Default blue markers work, but custom icons make your map unique. In Leaflet, use the pointToLayer function to replace the default marker. You can also use the style option to dynamically style polygons and lines based on feature properties.
// Leaflet: Custom icon and conditional styling
const greenIcon = L.icon({
iconUrl: 'marker-green.png',
iconSize: [25, 41]
});
L.geoJSON(geojsonData, {
pointToLayer: function (feature, latlng) {
return L.marker(latlng, { icon: greenIcon });
},
style: function (feature) {
return {
color: feature.properties.status === 'active' ? 'green' : 'red',
weight: 2
};
},
onEachFeature: function (feature, layer) {
const popupContent = `
<h3>${feature.properties.name}</h3>
<p>Status: <strong>${feature.properties.status}</strong></p>
<p>${feature.properties.description}</p>
`;
layer.bindPopup(popupContent);
}
}).addTo(map);
In Mapbox GL JS, you define styles in the layer’s layout and paint properties, often using expressions to make them data-driven.
Creating a Choropleth Map from GeoJSON Properties
A choropleth map colors regions based on a data property, like population density. Here’s how to build one with Leaflet. The logic is simple: define a function that maps your data values to a color scale, then apply it via the style option.
// Example: Color US states by population density
function getColor(density) {
return density > 500 ? '#800026' :
density > 200 ? '#BD0026' :
density > 100 ? '#E31A1C' :
density > 50 ? '#FC4E2A' :
density > 20 ? '#FD8D3C' :
density > 10 ? '#FEB24C' :
'#FFEDA0';
}
L.geoJSON(stateData, {
style: function(feature) {
return {
fillColor: getColor(feature.properties.density),
weight: 2,
opacity: 1,
color: 'white',
dashArray: '3',
fillOpacity: 0.7
};
},
onEachFeature: function (feature, layer) {
layer.bindPopup(`
<b>${feature.properties.name}</b><br />
Density: ${feature.properties.density} people/sq mi
`);
}
}).addTo(map);
For more advanced analysis directly in the browser, integrate Turf.js. It’s a spatial analysis library that works seamlessly with GeoJSON, allowing you to calculate distances, create buffers, find points within polygons, and much more.
Common GeoJSON Pitfalls and How to Avoid Them
Even experienced developers stumble on these issues. Knowing them upfront saves hours of debugging.
Coordinate Confusion: [Longitude, Latitude] is the Rule
The single most common error is swapping coordinate order. GeoJSON specifies [longitude, latitude] (X, Y). This is the opposite of Google Maps’ LatLng and feels counterintuitive. Getting it wrong will place your features in the wrong hemisphere or cause silent failures.
// WRONG: This will plot a point in the Indian Ocean near Somalia.
{ "type": "Point", "coordinates": [ 37.7749, -122.4194 ] } // [lat, lng]
// CORRECT: This plots San Francisco correctly.
{ "type": "Point", "coordinates": [ -122.4194, 37.7749 ] } // [lng, lat]
Other common pitfalls include using a Polygon when your data has multiple separate shapes (you need a MultiPolygon), or having a malformed properties object (it must be an object, even if empty {}).
Optimizing Performance for Large Datasets
Loading a GeoJSON file with ten thousand polygons will crash the browser. Performance strategies are essential.
- Simplify Geometries: Reduce the number of points in your shapes. Use the
simplify()function from Turf.js or a desktop tool likemapshaperbefore serving the data. - Implement Clustering: For point data, use a plugin like Leaflet.markercluster. It groups nearby points into a single cluster, dramatically reducing the number of DOM elements.
- Use Vector Tiles: For massive datasets (country-wide building footprints, global roads), GeoJSON is the wrong format. Convert your data to vector tiles (like MBTiles or PBF) and serve them using a library like Mapbox GL JS, MapLibre GL JS, or via a tile server. This allows the browser to only load data for the current viewport at the appropriate zoom level.
GeoJSON in the Modern Stack: React & Next.js Integration
Most modern frontends are built with frameworks. Using vanilla Leaflet or Mapbox GL JS directly in React can lead to conflicts with React’s virtual DOM. The solution is to use dedicated React wrapper libraries.
For Leaflet, use React Leaflet. It provides React components that manage the underlying Leaflet instances, ensuring proper lifecycle management.
import { MapContainer, TileLayer, GeoJSON } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
function MyMap() {
const geojsonData = { /* ... your GeoJSON ... */ };
return (
<MapContainer center={[51.505, -0.09]} zoom={13} style={{ height: '500px' }}>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<GeoJSON data={geojsonData} />
</MapContainer>
);
}
In Next.js, maps are client-side components. You must dynamically import them with next/dynamic and disable server-side rendering (ssr: false) to avoid window/document errors during the server build.
// In a Next.js page or component
import dynamic from 'next/dynamic';
const Map = dynamic(() => import('../components/MyMap'), {
ssr: false // This line is crucial
});
export default function HomePage() {
return <Map />;
}
For Mapbox GL JS in React, consider using react-map-gl. The pattern is similar: use the framework-specific library to ensure clean integration and state management.
GeoJSON is the foundational layer of modern web cartography. By mastering its structure and learning how to effectively wield it with libraries like Leaflet, Mapbox GL JS, and OpenLayers, you unlock the ability to turn raw geographic data into compelling, interactive user experiences. Start with simple points and lines, experiment with choropleths, and remember to optimize your data for performance. The map is now your canvas.