Migrate a Mapbox GL JS Map to Maptoolkit Maps JS
Most of a Mapbox GL JS application does not change when it moves to Maps JS, because Maps JS is MapLibre GL JS with the Maptoolkit services wired in, and MapLibre is the open 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 maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
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 maptoolkit.NavigationControl(), 'top-right');
new maptoolkit.Marker({ color: '#c0392b' })
.setLngLat([11.39085, 47.27574])
.setPopup(new maptoolkit.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 maptoolkit.Popup()
.setLngLat(e.features[0].geometry.coordinates)
.setHTML(`Size ${e.features[0].properties.size}`)
.addTo(map);
});
});<!DOCTYPE html>
<html lang="en">
<head>
<title>Migrated from Mapbox GL JS - Maptoolkit Maps JS</title>
<meta property="og:description" content="A Mapbox GL JS map ported to Maps JS." />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.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 maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
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 maptoolkit.NavigationControl(), 'top-right');
new maptoolkit.Marker({ color: '#c0392b' })
.setLngLat([11.39085, 47.27574])
.setPopup(new maptoolkit.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 maptoolkit.Popup()
.setLngLat(e.features[0].geometry.coordinates)
.setHTML(`Size ${e.features[0].properties.size}`)
.addTo(map);
});
});
</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
Four 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://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.js"></script>
+ <link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.css" />
- mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
- const map = new mapboxgl.Map({
+ const map = new maptoolkit.Map({
container: 'map',
+ apiKey: API_KEY,
- style: 'mapbox://styles/mapbox/outdoors-v12',
+ style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [11.39085, 47.27574],
zoom: 12
});
- map.addControl(new mapboxgl.NavigationControl(), 'top-right');
+ map.addControl(new maptoolkit.NavigationControl(), 'top-right');
- new mapboxgl.Marker({ color: '#c0392b' })
+ new maptoolkit.Marker({ color: '#c0392b' })
.setLngLat([11.39085, 47.27574])
- .setPopup(new mapboxgl.Popup().setHTML('<strong>Innsbruck</strong>'))
+ .setPopup(new maptoolkit.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
maptoolkit. 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 two things, not one. mapboxgl.accessToken was a global; the
Maptoolkit key is an apiKey constructor option and a query parameter on the style URL.
Setting only the constructor option leaves the style URL unauthenticated and the map loads
blank; setting only the style URL leaves the SDK’s own service calls unauthenticated. Set
both, from one constant.
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. This is the part people expect to lose
and do not.
What Maps JS adds beyond plain MapLibre is the reason to pick it over the fork: the
StyleControl, TerrainControl and IsochroneControl are one line each rather than a fetch
and a layer, and the key is handled for the SDK’s own service calls.
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, which is the single most expensive thing to miss in this migration.
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, the controls are what justify the move. A style switcher and 3D terrain are a line each, and the same API key already authorizes the Routing API, Geocoding API and Isochrone API.