Guide

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.

The changes for a typical map, with a bundler:
- 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. becomes maplibregl.: Map, Marker, Popup, NavigationControl, GeolocateControl, ScaleControl, FullscreenControl and LngLatBounds keep their names and options.
  • Delete mapboxgl.accessToken and the accessToken option. The key of your tile provider goes into the style URL instead.
  • CSS classes start with maplibregl-: rename .mapboxgl-popup, .mapboxgl-marker, .mapboxgl-ctrl and the others in your own stylesheets.
  • Events, addSource, addLayer, expressions, setPaintProperty, queryRenderedFeatures, fitBounds, flyTo and transformRequest work 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 JSIn MapLibre
The Mapbox Standard style, setConfigProperty and light presetsPick a style; switch between a light and a dark style with setStyle.
The slot property of layersThe beforeId argument of addLayer(layer, beforeId).
The Interactions API (addInteraction)map.on("click", layerId, handler) and feature state.
setFogsetSky.
Mapbox Search and DirectionsA geocoding or routing API of your choice, through the plugins below.

7. Plugins

MapboxMapLibre
@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-drawworks, after setting its class names (below), or terra-draw with its MapLibre adapter
react-map-glthe 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

  1. Swap the package, imports and CSS file; delete the access token.
  2. With a bundler, call setWorkerUrl once.
  3. Rename .mapboxgl- classes in your CSS.
  4. Replace every mapbox:// URL: style, terrain, tilesets, sprites and fonts.
  5. Replace Standard style options, slot, setFog and the Interactions API, if you use them.
  6. Swap the plugins.
  7. Check the credit in the corner of the map.