Migrate a Mapbox GL JS Map to MapLibre GL JS
Most of a Mapbox GL JS application does not change when it moves to MapLibre GL JS, because MapLibre is the open-source fork of Mapbox GL JS v1. The map below is the finished port, running. Under it is the complete diff against the Mapbox GL JS original, so you can see exactly how little of it changed.
The original is not embedded beside it, because Mapbox GL JS v2 and later refuses to start without a Mapbox access token: a runnable “before” would mean asking you to supply one and be billed by Mapbox to look at a migration guide. The diff is the honest substitute.
const API_KEY = 'YOUR_API_KEY';
const map = new maplibregl.Map({
container: 'map',
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [11.39085, 47.27574],
zoom: 12,
attributionControl: { compact: false }
});
// Everything below this line is unchanged from the Mapbox GL JS original.
map.addControl(new maplibregl.NavigationControl(), 'top-right');
new maplibregl.Marker({ color: '#c0392b' })
.setLngLat([11.39085, 47.27574])
.setPopup(new maplibregl.Popup().setHTML('<strong>Innsbruck</strong>'))
.addTo(map);
map.on('load', () => {
map.addSource('trail', {
type: 'geojson',
data: {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: [[11.3857, 47.2683], [11.3950, 47.2760], [11.4100, 47.2830], [11.4300, 47.2900]]
}
}
});
map.addLayer({
id: 'trail',
type: 'line',
source: 'trail',
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { 'line-color': '#2a3561', 'line-width': 5 }
});
map.addSource('stops', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{ type: 'Feature', properties: { size: 3 }, geometry: { type: 'Point', coordinates: [11.3857, 47.2683] } },
{ type: 'Feature', properties: { size: 7 }, geometry: { type: 'Point', coordinates: [11.4100, 47.2830] } },
{ type: 'Feature', properties: { size: 5 }, geometry: { type: 'Point', coordinates: [11.4300, 47.2900] } }
]
}
});
map.addLayer({
id: 'stops',
type: 'circle',
source: 'stops',
paint: {
'circle-radius': ['interpolate', ['linear'], ['get', 'size'], 1, 5, 10, 16],
'circle-color': '#e8710a',
'circle-stroke-color': '#fff',
'circle-stroke-width': 2
}
});
map.on('click', 'stops', (e) => {
new maplibregl.Popup()
.setLngLat(e.features[0].geometry.coordinates)
.setHTML(`Size ${e.features[0].properties.size}`)
.addTo(map);
});
});
import { LogoControl } from "https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.js";
map.addControl(new LogoControl({ variant: "com" }));<!DOCTYPE html>
<html lang="en">
<head>
<title>Migrated from Mapbox GL JS - MapLibre GL JS</title>
<meta property="og:description" content="A Mapbox GL JS map ported to MapLibre GL JS." />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.css" />
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
#map { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
const API_KEY = 'YOUR_API_KEY';
const map = new maplibregl.Map({
container: 'map',
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [11.39085, 47.27574],
zoom: 12,
attributionControl: { compact: false }
});
// Everything below this line is unchanged from the Mapbox GL JS original.
map.addControl(new maplibregl.NavigationControl(), 'top-right');
new maplibregl.Marker({ color: '#c0392b' })
.setLngLat([11.39085, 47.27574])
.setPopup(new maplibregl.Popup().setHTML('<strong>Innsbruck</strong>'))
.addTo(map);
map.on('load', () => {
map.addSource('trail', {
type: 'geojson',
data: {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: [[11.3857, 47.2683], [11.3950, 47.2760], [11.4100, 47.2830], [11.4300, 47.2900]]
}
}
});
map.addLayer({
id: 'trail',
type: 'line',
source: 'trail',
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { 'line-color': '#2a3561', 'line-width': 5 }
});
map.addSource('stops', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{ type: 'Feature', properties: { size: 3 }, geometry: { type: 'Point', coordinates: [11.3857, 47.2683] } },
{ type: 'Feature', properties: { size: 7 }, geometry: { type: 'Point', coordinates: [11.4100, 47.2830] } },
{ type: 'Feature', properties: { size: 5 }, geometry: { type: 'Point', coordinates: [11.4300, 47.2900] } }
]
}
});
map.addLayer({
id: 'stops',
type: 'circle',
source: 'stops',
paint: {
'circle-radius': ['interpolate', ['linear'], ['get', 'size'], 1, 5, 10, 16],
'circle-color': '#e8710a',
'circle-stroke-color': '#fff',
'circle-stroke-width': 2
}
});
map.on('click', 'stops', (e) => {
new maplibregl.Popup()
.setLngLat(e.features[0].geometry.coordinates)
.setHTML(`Size ${e.features[0].properties.size}`)
.addTo(map);
});
});
</script>
<script type="module">
import { LogoControl } from "https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.js";
map.addControl(new LogoControl({ variant: "com" }));
</script>
</body>
</html>Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.
The complete diff
Five lines change in the setup, and the namespace changes throughout. Nothing else does.
- <script src="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
- <link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css" />
+ <script src="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.js"></script>
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.css" />
- mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
- const map = new mapboxgl.Map({
+ const map = new maplibregl.Map({
container: 'map',
- style: 'mapbox://styles/mapbox/outdoors-v12',
+ style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
+ attributionControl: { compact: false },
center: [11.39085, 47.27574],
zoom: 12
});
- map.addControl(new mapboxgl.NavigationControl(), 'top-right');
+ map.addControl(new maplibregl.NavigationControl(), 'top-right');
- new mapboxgl.Marker({ color: '#c0392b' })
+ new maplibregl.Marker({ color: '#c0392b' })
.setLngLat([11.39085, 47.27574])
- .setPopup(new mapboxgl.Popup().setHTML('<strong>Innsbruck</strong>'))
+ .setPopup(new maplibregl.Popup().setHTML('<strong>Innsbruck</strong>'))
.addTo(map);
Every addSource, addLayer, paint property, expression and event handler in the example
above is byte for byte what it was on Mapbox GL JS.
How it works
The namespace is the bulk of the change, and it is mechanical. mapboxgl. becomes
maplibregl. for Map, Marker, Popup, LngLat, LngLatBounds, MercatorCoordinate,
NavigationControl, ScaleControl and the rest. A find and replace on mapboxgl. handles a
codebase in one pass.
The access token becomes a query parameter. mapboxgl.accessToken was a global; the
Maptoolkit key goes on the style URL as ?api_key=. MapLibre does not add it anywhere else,
so every Maptoolkit URL you request yourself, such as a terrain source or a call to the
Routing API, needs the same parameter. Keep the key in one constant and build every URL
from it.
mapbox:// URLs have no equivalent. Styles, sprites, glyphs and tile sources all have to
become real HTTPS URLs. That is usually the only part of a migration that takes real work,
because a custom Mapbox Studio style also has to be rebuilt: the layers reference the Mapbox
Streets schema, and the field names differ from ours. The
Schema Reference is what you remap against.
The expression language is identical, so interpolate, step, match, case,
feature-state and the rest all carry over untouched.
Maptoolkit adds its own controls as MapLibre plugins: a style switcher, an
isochrone control, the Maptoolkit logo and a map legend. Each is one addControl call.
TerrainControl is built into MapLibre GL JS.
Watch terrain specifically. Maptoolkit DEM tiles are Terrarium encoded and Mapbox uses
terrain-rgb. A copied raster-dem source block loads without an error and renders plausible
but wrong elevation, so check the encoding of every copied terrain source.
Next steps
The migration guide covers the parts that are not code: which Mapbox services map to which of ours, what has no equivalent at all, and how tile and API requests are metered differently.
Once the map is running, a style switcher and 3D terrain take a few lines each, and the same API key already authorizes the Routing API, Geocoding API and Isochrone API.