From Mapbox GL JS to MapLibre GL JS
MapLibre GL JS began in December 2020 as a fork of Mapbox GL JS 1.13, when Mapbox moved version 2 to a proprietary licence. The API has stayed close, so most maps move with a few changed lines. What changes, what has no counterpart in MapLibre, and what to do about styles and tiles. Tested with MapLibre GL JS 6.13.0.
- import mapboxgl from "mapbox-gl"; - import "mapbox-gl/dist/mapbox-gl.css"; - mapboxgl.accessToken = "pk.…"; + import * as maplibregl from "maplibre-gl"; + import "maplibre-gl/dist/maplibre-gl.css"; - const map = new mapboxgl.Map({ + const map = new maplibregl.Map({ container: "map", - style: "mapbox://styles/mapbox/streets-v12", + style: "https://api.overpass.ch/v1/YOUR_KEY/maps/styles/light.json", center: [8.54, 47.37], zoom: 12 });With Vite, webpack or another bundler, MapLibre 6 also needs one
setWorkerUrl call.
1. Replace the package
npm uninstall mapbox-gl @types/mapbox-gl npm install maplibre-gl
MapLibre ships its own TypeScript types. MapLibre 6 comes as ES modules only, so the default import of Mapbox (import mapboxgl from "mapbox-gl") becomes a namespace import or named imports:
import * as maplibregl from "maplibre-gl";
// or
import { Map, NavigationControl, Marker, Popup } from "maplibre-gl";
Without a bundler, load the module from a CDN. A classic <script src> tag no longer works with version 6:
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@6.13.0/dist/maplibre-gl.css">
<script type="module">
import * as maplibregl from "https://unpkg.com/maplibre-gl@6.13.0/dist/maplibre-gl.mjs";
// your map code
</script>
Pin the version in CDN URLs. Code that must keep a classic script tag can use MapLibre 5, the last version with that build (maplibre-gl@5.24.0/dist/maplibre-gl.js, global maplibregl).
2. With a bundler: set the worker URL
MapLibre parses tiles in a web worker. With a bundler, MapLibre 6 does not find the worker file by itself, and the map stays empty with this error in the console:
Error: Worker failed to load. Check that the worker URL is correct.
Set the URL once, before the first map. For Vite, as we tested with Vite 8:
import { setWorkerUrl } from "maplibre-gl";
import workerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url";
setWorkerUrl(workerUrl);
For webpack 5, rspack and Next.js, the MapLibre documentation gives setWorkerUrl(new URL("maplibre-gl/dist/maplibre-gl-worker.mjs", import.meta.url).toString()). The CDN module above needs no call.
3. Rename and remove
mapboxgl.becomesmaplibregl.:Map,Marker,Popup,NavigationControl,GeolocateControl,ScaleControl,FullscreenControlandLngLatBoundskeep their names and options.- Delete
mapboxgl.accessTokenand theaccessTokenoption. The key of your tile provider goes into the style URL instead. - CSS classes start with
maplibregl-: rename.mapboxgl-popup,.mapboxgl-marker,.mapboxgl-ctrland the others in your own stylesheets. - Events,
addSource,addLayer, expressions,setPaintProperty,queryRenderedFeatures,fitBounds,flyToandtransformRequestwork as before. - MapLibre 6 needs WebGL 2, which current browsers have.
This MapLibre code ran unchanged after the renaming above: a marker with a popup, a GeoJSON layer with a click handler, and the globe.
const map = new maplibregl.Map({
container: "map",
style: "https://api.overpass.ch/v1/YOUR_KEY/maps/styles/light.json",
center: [8.54, 47.37],
zoom: 12
});
map.addControl(new maplibregl.NavigationControl());
new maplibregl.Marker({ color: "#d0312d" })
.setLngLat([8.5417, 47.3769])
.setPopup(new maplibregl.Popup().setText("Zürich HB"))
.addTo(map);
map.on("load", () => {
map.addSource("stops", { type: "geojson", data: "/stops.geojson" });
map.addLayer({ id: "stops", type: "circle", source: "stops",
paint: { "circle-radius": 6, "circle-color": "#1f5fbf" } });
map.on("click", "stops", (e) => {
new maplibregl.Popup().setLngLat(e.lngLat).setText(e.features[0].properties.name).addTo(map);
});
map.setProjection({ type: "globe" }); // Mapbox: projection: "globe"
});
4. Styles and tiles
MapLibre does not resolve mapbox:// URLs, and the Mapbox styles and tiles belong to your Mapbox account and its terms. After the move you need a tile provider and a style for its tiles:
- Use a ready-made style URL. Ours are listed on the map tiles page: light, dark, white, grayscale, black, and our own outdoor, marine, relief and driving styles, with labels in 41 languages.
- A style made in Mapbox Studio uses the layers of the Mapbox Streets tiles. Other tiles have other layers: ours follow the Protomaps schema, others OpenMapTiles. Rebuild the look on one of the new styles, for example in Maputnik, and serve the JSON from your site.
- Your own GeoJSON and vector tile sources move unchanged. Mapbox-hosted tilesets (
mapbox://username.tileset) need a new home, such as a PMTiles file on your own storage.
5. Terrain, sky and globe
// Mapbox map.addSource("mapbox-dem", { type: "raster-dem", url: "mapbox://mapbox.mapbox-terrain-dem-v1" }); map.setTerrain({ source: "mapbox-dem", exaggeration: 1.5 }); map.setFog({}); // MapLibre map.addSource("terrain", { type: "raster-dem", url: "https://api.overpass.ch/v1/YOUR_KEY/maps/terrain.json" }); map.setTerrain({ source: "terrain", exaggeration: 1.5 }); map.setSky({ "sky-color": "#7fb2e5", "horizon-color": "#dfeaf5", "fog-color": "#ffffff" });
Our terrain tiles use the Mapbox Terrain-RGB encoding, the default of MapLibre. setFog has no counterpart; setSky sets sky, horizon and fog colours with other property names. The guide 3D terrain in MapLibre has more.
6. Mapbox features without a counterpart
These Mapbox GL JS 2 and 3 features do not exist in MapLibre. Each needs other code or a design decision:
| Mapbox GL JS | In MapLibre |
|---|---|
The Mapbox Standard style, setConfigProperty and light presets | Pick a style; switch between a light and a dark style with setStyle. |
The slot property of layers | The beforeId argument of addLayer(layer, beforeId). |
The Interactions API (addInteraction) | map.on("click", layerId, handler) and feature state. |
setFog | setSky. |
| Mapbox Search and Directions | A geocoding or routing API of your choice, through the plugins below. |
7. Plugins
| Mapbox | MapLibre |
|---|---|
@mapbox/mapbox-gl-geocoder | @maplibre/maplibre-gl-geocoder, with a geocoding API of your choice behind it |
@mapbox/mapbox-gl-directions | @maplibre/maplibre-gl-directions (version 0.10 for MapLibre 6) |
@mapbox/mapbox-gl-draw | works, after setting its class names (below), or terra-draw with its MapLibre adapter |
react-map-gl | the same package, imported from react-map-gl/maplibre; see react-map-gl with MapLibre |
mapbox-gl-draw 1.5.2 ran on MapLibre 6.13.0 in our test, with its buttons, polygons and modes, after these lines before new MapboxDraw():
MapboxDraw.constants.classes.CANVAS = "maplibregl-canvas"; MapboxDraw.constants.classes.CONTROL_BASE = "maplibregl-ctrl"; MapboxDraw.constants.classes.CONTROL_PREFIX = "maplibregl-ctrl-"; MapboxDraw.constants.classes.CONTROL_GROUP = "maplibregl-ctrl-group"; MapboxDraw.constants.classes.ATTRIBUTION = "maplibregl-ctrl-attrib";
8. Attribution and billing
- The Mapbox logo and the Mapbox credit go, the credit of the new data stays. Our styles add the OpenStreetMap credit to the attribution control by themselves; keep it visible.
- Mapbox GL JS is billed per map load. MapLibre has no billing of its own; your tile provider bills tiles. overpass.ch counts each tile once, and every account includes 100'000 tiles a month. What that means at your traffic is in Map tile API prices compared.
Checklist
- Swap the package, imports and CSS file; delete the access token.
- With a bundler, call
setWorkerUrlonce. - Rename
.mapboxgl-classes in your CSS. - Replace every
mapbox://URL: style, terrain, tilesets, sprites and fonts. - Replace Standard style options,
slot,setFogand the Interactions API, if you use them. - Swap the plugins.
- Check the credit in the corner of the map.