Introduction
Mapbox GL JS is a powerful open-source JavaScript library for displaying interactive vector and 3D maps in web browsers. In 2026, it remains the top choice for apps needing fluid rendering, infinite zooms, and extreme style customization. Unlike traditional raster solutions like Leaflet with image tiles, Mapbox leverages WebGL for GPU-accelerated rendering, perfect for geospatial dashboards, delivery apps, or immersive visualizations.
This intermediate tutorial takes you from basic initialization to advanced features like dynamic GeoJSON layers, interactive events, and flyTo animations. You'll get complete, copy-paste-ready code examples for instant integration. By the end, you'll know how to optimize performance and sidestep common pitfalls. Grab your free Mapbox token and follow this progressive setup to build a full-featured map in just a few steps.
Prerequisites
- A free Mapbox account to get a public access token (create one at account.mapbox.com).
- Basic knowledge of HTML, CSS, and modern JavaScript (ES6+).
- A code editor like VS Code and a local server (Live Server or
npx serve). - A recent browser with WebGL support (Chrome, Firefox).
Basic Map Initialization
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mapbox GL JS Basique</title>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.css" rel="stylesheet" />
<style>
body { margin: 0; padding: 0; }
#map { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoidXNlcn9ub20iLCJhIjoiY29kZV90b2tlbiJ9'; // Replace with YOUR_TOKEN
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [2.3522, 48.8566], // Paris
zoom: 10
});
</script>
</body>
</html>This standalone HTML file initializes a map centered on Paris using the 'streets-v12' style. The Mapbox token is required for API requests—replace it with your own. The #map container fills the full viewport for an immersive render. Open this file in a browser to see the basic interactive map.
Adding Navigation Controls
Mapbox's built-in controls enhance UX with zoom, rotation, and GPS positioning. Add them after the map's load event to avoid timing issues. Think of them as cockpit buttons: intuitive and essential for navigation.
Adding NavigationControl and GeolocateControl
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mapbox avec Contrôles</title>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.css" rel="stylesheet" />
<style>
body { margin: 0; padding: 0; }
#map { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoidXNlcn9ub20iLCJhIjoiY29kZV90b2tlbiJ9'; // YOUR_TOKEN
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [2.3522, 48.8566],
zoom: 10
});
map.on('load', () => {
map.addControl(new mapboxgl.NavigationControl({ showCompass: true }));
map.addControl(new mapboxgl.GeolocateControl({
positionOptions: { enableHighAccuracy: true },
trackUserLocation: true
}));
});
</script>
</body>
</html>After loading (map.on('load')), NavigationControl adds zoom/rotation, and GeolocateControl enables geolocation. showCompass: true activates the compass. This prevents premature additions that crash if the map isn't ready. Test by clicking the GPS button.
Adding Markers and GeoJSON Layers
For custom data, use Popup markers or GeoJSON sources. Vector layers render client-side for infinite scalability, like overlaying dynamic data on a satellite base.
Adding a Marker with Popup and GeoJSON Layer
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mapbox Marqueurs et GeoJSON</title>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.css" rel="stylesheet" />
<style>
body { margin: 0; padding: 0; }
#map { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoidXNlcn9ub20iLCJhIjoiY29kZV90b2tlbiJ9'; // YOUR_TOKEN
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [2.3522, 48.8566],
zoom: 10
});
map.on('load', () => {
// Marker with popup
const marker = new mapboxgl.Marker({ color: 'red' })
.setLngLat([2.3522, 48.8566])
.setPopup(new mapboxgl.Popup().setHTML('<h3>Tour Eiffel</h3><p>📍 Icône de Paris</p>'))
.addTo(map);
// Example GeoJSON layer (parks in Paris)
map.addSource('parcs', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [{
type: 'Feature',
geometry: { type: 'Polygon', coordinates: [[[2.29, 48.86], [2.30, 48.86], [2.30, 48.85], [2.29, 48.85], [2.29, 48.86]]] },
properties: { nom: 'Parc exemple' }
}]
}
});
map.addLayer({
id: 'parcs-fill',
type: 'fill',
source: 'parcs',
paint: { 'fill-color': 'green', 'fill-opacity': 0.5 }
});
});
</script>
</body>
</html>A red marker with a popup appears in Paris—click to see the HTML content. The GeoJSON source adds a green polygonal layer for a fictional park, scalable to thousands of features via fetch(). Layers overlay seamlessly thanks to vector rendering.
Handling Interactive Events
Capture clicks, movements, or zooms for dynamic interactions, like contextual tooltips or data filters. This is the heart of reactive apps.
Click Events and flyTo Animation
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mapbox Événements Interactifs</title>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.css" rel="stylesheet" />
<style>
body { margin: 0; padding: 0; }
#map { width: 100vw; height: 100vh; }
button { position: absolute; top: 10px; left: 10px; z-index: 1; }
</style>
</head>
<body>
<div id="map"></div>
<button onclick="flyToLyon()">Voler vers Lyon</button>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoidXNlcn9ub20iLCJhIjoiY29kZV90b2tlbiJ9'; // YOUR_TOKEN
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [2.3522, 48.8566],
zoom: 10
});
map.on('load', () => {
map.on('click', (e) => {
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML(`<p>Clic à ${e.lngLat.lat.toFixed(4)}, ${e.lngLat.lng.toFixed(4)}</p>`)
.addTo(map);
});
});
function flyToLyon() {
map.flyTo({
center: [4.8357, 45.7640],
zoom: 12,
essential: true,
duration: 3000
});
}
</script>
</body>
</html>The click event adds a popup at clicked coordinates. The button triggers flyTo for a smooth 3-second animation to Lyon. essential: true prioritizes GPU animation. This makes the map lively and responsive to user input.
3D Customization and Advanced Styles
Enable 3D terrain or custom styles for immersive visualizations. Mapbox expressions enable dynamic, data-driven rendering.
Enabling 3D Mode with Terrain and Extrusion
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mapbox 3D et Terrain</title>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.css" rel="stylesheet" />
<style>
body { margin: 0; padding: 0; }
#map { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoidXNlcn9ub20iLCJhIjoiY29kZV90b2tlbiJ9'; // YOUR_TOKEN
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/satellite-streets-v12',
center: [2.3522, 48.8566],
zoom: 12,
pitch: 45,
bearing: -17.6
});
map.on('style.load', () => {
map.setTerrain({ source: 'mapbox-dem', exaggeration: 1.5 });
map.addLayer({
id: 'buildings-3d',
type: 'fill-extrusion',
source: 'composite',
source-layer: 'building',
paint: {
'fill-extrusion-color': '#aaa',
'fill-extrusion-height': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.05, ['get', 'height']],
'fill-extrusion-base': 0,
'fill-extrusion-opacity': 0.6
}
});
});
</script>
</body>
</html>Satellite style with pitch and bearing for an oblique view. setTerrain enables 3D relief via DEM; fill-extrusion extrudes buildings based on height. Use style.load to add layers after loading. Perfect for immersive urban visualizations.
Optimization with GeoJSON Clustering
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mapbox Clusters Optimisés</title>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.css" rel="stylesheet" />
<style>
body { margin: 0; padding: 0; }
#map { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoidXNlcn9ub20iLCJhIjoiY29kZV90b2tlbiJ9'; // YOUR_TOKEN
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [2.3522, 48.8566],
zoom: 10
});
map.on('load', () => {
map.addSource('points', {
type: 'geojson',
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50,
data: {
type: 'FeatureCollection',
features: Array.from({length: 100}, (_, i) => ({
type: 'Feature',
geometry: { type: 'Point', coordinates: [2.35 + Math.random()*0.02, 48.85 + Math.random()*0.02] },
properties: { point_count: 1 }
}))
}
});
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'points',
filter: ['has', 'point_count'],
paint: {
'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 100, '#f1f075', 750, '#f28cb1'],
'circle-radius': ['step', ['get', 'point_count'], 20, 100, 30, 750, 40]
}
});
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'points',
filter: ['has', 'point_count'],
layout: { 'text-field': '{point_count_abbreviated}' }
});
map.on('click', 'clusters', (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
map.getSource('points').getClusterExpansionZoom(features[0].properties.cluster_id, (zoom) => {
map.easeTo({ center: features[0].geometry.coordinates, zoom });
});
});
});
</script>
</body>
</html>Generates 100 random points around Paris with automatic clustering (cluster: true). Circles change size/color by density; click to expand. Ideal for massive datasets (millions of points) without performance loss, thanks to vector clustering.
Best Practices
- Always use
map.on('load')orstyle.loadfor post-init additions to avoid missing source errors. - Hide your token in production: Use environment variables or a backend proxy for public apps.
- Limit layers to 50 max; merge them for GPU performance.
- Test WebGL with
map.isStyleLoaded()and fallback to raster if needed. - Use custom styles from Mapbox Studio for data-driven styling.
Common Errors to Avoid
- Invalid or missing token: 401/403 error; check public vs. secret and quotas (free: 50k users/month).
- Adding layers before
load: Nothing shows; wrap in the event. - Missing
filteron layers: Messy clusters or popups; use['==', 'type', 'Feature']. - Memory leaks on events: Use
off()beforeon()to avoid duplicates on React/Vue re-renders.
Next Steps
Check the official Mapbox GL JS docs for offline maps or GLTF 3D. Integrate with React using react-map-gl. For pro cartography mastery, explore our Learni advanced JavaScript and geospatial training.