Introduction
Mapbox GL JS is the go-to JavaScript library for rendering interactive vector maps in the browser, powered by WebGL for exceptional performance even with millions of data points. In 2026, it includes native optimizations for Progressive Web Apps (PWAs), advanced 3D rendering, and AI-driven predictive geolocation, making it perfect for analytics dashboards, delivery apps, geolocated e-commerce sites, or IoT visualizations.
This intermediate tutorial takes you from A to Z to build a professional map: basic initialization, navigation controls, custom markers with popups, dynamic GeoJSON layers, and event handling. Every step provides complete, functional code you can copy-paste into a single HTML file. By the end, you'll handle common pitfalls and best practices for production scaling. Think Google Maps-level smoothness, but fully customizable and free to start. Ready to turn your data into immersive maps?
Prerequisites
- A free Mapbox account to get a public access token (starts with
pk.). - Basic knowledge of HTML, CSS, and JavaScript (ES6+).
- A code editor like VS Code with the Live Server extension for local serving (avoids CORS errors).
- A modern browser (Chrome 100+, Firefox 100+).
Basic HTML structure and map initialization
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Carte Mapbox GL JS Basique</title>
<link href="https://api.mapbox.com/mapbox-gl-js/v2.17.0/mapbox-gl.css" rel="stylesheet">
<style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script src="https://api.mapbox.com/mapbox-gl-js/v2.17.0/mapbox-gl.js"></script>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoidXNlcm5hbWUiLCJhIjoiY29udGVudCJ9.SECRET_TOKEN'; // Remplacez par votre token public
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [2.3522, 48.8566], // Longitude, latitude Paris
zoom: 10
});
</script>
</body>
</html>This standalone HTML page loads Mapbox GL JS via CDN and initializes a map centered on Paris using the 'streets-v12' style. The #map container fills the full screen with absolute CSS positioning. Replace the placeholder token with your own from the Mapbox dashboard; use Live Server to test instantly and avoid issues.
Adding navigation controls
Standard controls for zoom, rotation, and position greatly improve UX with minimal effort. Mapbox provides ready-to-use classes like NavigationControl and GeolocateControl. Add them after the map loads via the 'load' event to avoid timing errors. This makes your map as intuitive as professional tools.
Map with navigation controls
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Carte avec Contrôles</title>
<link href="https://api.mapbox.com/mapbox-gl-js/v2.17.0/mapbox-gl.css" rel="stylesheet">
<style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script src="https://api.mapbox.com/mapbox-gl-js/v2.17.0/mapbox-gl.js"></script>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoidXNlcm5hbWUiLCJhIjoiY29udGVudCJ9.SECRET_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>This extended version adds NavigationControl for zoom/pan/compass and GeolocateControl for GPS location. The map.on('load') event ensures the map is ready. enableHighAccuracy: true improves mobile precision; never add controls before 'load' to prevent crashes.
Adding custom markers
Markers offer simplicity and drag-and-drop functionality that beats SVG icons. Use the Marker class for custom pins with HTML/CSS, ideal for points of interest like stores or events.
Adding a custom marker
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Carte avec Marker</title>
<link href="https://api.mapbox.com/mapbox-gl-js/v2.17.0/mapbox-gl.css" rel="stylesheet">
<style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
.marker-custom {
background: #ff0000;
width: 30px;
height: 30px;
border-radius: 50%;
border: 3px solid white;
cursor: pointer;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
</style>
</head>
<body>
<div id="map"></div>
<script src="https://api.mapbox.com/mapbox-gl-js/v2.17.0/mapbox-gl.js"></script>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoidXNlcm5hbWUiLCJhIjoiY29udGVudCJ9.SECRET_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());
map.addControl(new mapboxgl.GeolocateControl({ positionOptions: { enableHighAccuracy: true } }));
const marker = new mapboxgl.Marker({ color: 'red', scale: 1.2 })
.setLngLat([2.3522, 48.8566])
.addTo(map);
});
</script>
</body>
</html>A custom red marker is added via new mapboxgl.Marker() at Paris coordinates, with color and scale options. Use CSS like .marker-custom via element: document.createElement('div') for more customization. More performant than layers for few points; dragging enabled by default.
Informative and interactive popups
Popups enrich markers with dynamic content like text, images, or links. Pair them with click events for engaging UX, such as showing event details.
Marker with popup
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Carte avec Popup</title>
<link href="https://api.mapbox.com/mapbox-gl-js/v2.17.0/mapbox-gl.css" rel="stylesheet">
<style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
.popup-content { padding: 10px; }
</style>
</head>
<body>
<div id="map"></div>
<script src="https://api.mapbox.com/mapbox-gl-js/v2.17.0/mapbox-gl.js"></script>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoidXNlcm5hbWUiLCJhIjoiY29udGVudCJ9.SECRET_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());
const popup = new mapboxgl.Popup({ offset: 25 }).setText('Tour Eiffel à proximité ! Cliquez pour + d\'infos.');
const marker = new mapboxgl.Marker({ color: 'orange' })
.setLngLat([2.2945, 48.8584])
.setPopup(popup)
.addTo(map);
});
</script>
</body>
</html>The Popup attaches to the marker via setPopup(), showing text or HTML with setHTML(). offset: 25 positions it nicely above the pin. Use closeButton: false for persistence; popups auto-adapt on mobile.
Custom layers with GeoJSON
For large datasets like roads or buildings, add GeoJSON sources and layers. It's vector-based, zoom-independent, and styleable via Mapbox Studio.
Adding a GeoJSON layer
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Carte avec GeoJSON Layer</title>
<link href="https://api.mapbox.com/mapbox-gl-js/v2.17.0/mapbox-gl.css" rel="stylesheet">
<style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
.geojson-point { background: blue; width: 20px; height: 20px; border-radius: 50%; }
</style>
</head>
<body>
<div id="map"></div>
<script src="https://api.mapbox.com/mapbox-gl-js/v2.17.0/mapbox-gl.js"></script>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoidXNlcm5hbWUiLCJhIjoiY29udGVudCJ9.SECRET_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [2.3522, 48.8566],
zoom: 12
});
map.on('load', () => {
map.addControl(new mapboxgl.NavigationControl());
map.addSource('points-sample', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [{
type: 'Feature',
geometry: { type: 'Point', coordinates: [2.3522, 48.8566] },
properties: { name: 'Point Paris' }
}]
}
});
map.addLayer({
id: 'points-layer',
type: 'circle',
source: 'points-sample',
paint: { 'circle-radius': 10, 'circle-color': '#007cbf' }
});
});
</script>
</body>
</html>An inline GeoJSON source powers a blue 'circle' layer. Use addSource then addLayer; properties enable filtering with filter. Scales well for fetch('/data.geojson'); use Mapbox Studio for complex styles without code.
Handling events and interactions
Bring your map to life with listeners for clicks, mouseenter, and zoomend. Ideal for dynamic tooltips or analytics.
Map events and interactions
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Carte avec Événements</title>
<link href="https://api.mapbox.com/mapbox-gl-js/v2.17.0/mapbox-gl.css" rel="stylesheet">
<style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
#info { position: absolute; top: 10px; left: 10px; background: white; padding: 10px; z-index: 1; }
</style>
</head>
<body>
<div id="info">Cliquez sur la carte !</div>
<div id="map"></div>
<script src="https://api.mapbox.com/mapbox-gl-js/v2.17.0/mapbox-gl.js"></script>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoidXNlcm5hbWUiLCJhIjoiY29udGVudCJ9.SECRET_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());
map.on('click', (e) => {
new mapboxgl.Popup().setLngLat(e.lngLat).setHTML(`<h3>Coords: ${e.lngLat.lat.toFixed(4)}, ${e.lngLat.lng.toFixed(4)}</h3>`).addTo(map);
});
map.on('mouseenter', 'points-layer', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'points-layer', () => { map.getCanvas().style.cursor = ''; });
});
</script>
</body>
</html>click listener opens a popup at clicked coordinates; mouseenter/leave changes cursor on layer. The #info overlay provides feedback. Clean up global handlers with map.off() for performance; great for querying features.
Best practices
- Secure tokens: Use public (
pk.) for frontend, secret (sk.) for backend only. - Limit bounds/zoom:
map.setMaxBounds()andzoom: [min, max]for regional focus. - Lazy-load data: Fetch GeoJSON after 'load', use
map.queryRenderedFeatures()for interactions. - Optimize performance: Cluster >1000 points with
supercluster, support dark/light themes. - Accessibility: Add
aria-labelto controls, enable keyboard nav with Mapbox focus API.
Common errors to avoid
- No local server: Opening HTML directly triggers CORS blocks on Mapbox CDN/tiles.
- Missing token: Results in a blank gray map; check console for 'Invalid access token'.
- Container without dimensions:
#mapneedsheight: 100vhor absolute positioning; otherwise invisible. - Adding layers before 'load': Causes crashes; always use
map.on('load', ...)and checkmap.isStyleLoaded().
Next steps
- Official docs: Mapbox GL JS Docs.
- Mapbox Studio for custom styles without code.
- Integrate with React via
react-map-glor Vue. - Check out our Learni courses on geospatial web development to master Turf.js and 3D advanced features.