Migrating Mapbox Styles to MapLibre GL JS
Most of a Mapbox style is valid MapLibre without modification: the spec MapLibre forked from is the same one, and layers, expressions and paint properties carry across unchanged. What does not carry across is everything that depended on Mapbox as a service — the mapbox:// URL scheme, the access token, the hosted tilesets, sprites and glyphs, and a handful of properties added to Mapbox GL JS after the fork.
When to Use This
Moving an existing map off Mapbox hosting, adopting a Mapbox-authored open style such as a Protomaps or OpenMapTiles derivative, or removing a token dependency from a public deployment.
Specification Detail: What Has to Change
| In the Mapbox style | In MapLibre | Why |
|---|---|---|
"url": "mapbox://mapbox.mapbox-streets-v8" |
An HTTPS TileJSON or PMTiles URL | MapLibre does not resolve the mapbox:// scheme |
"sprite": "mapbox://sprites/…" |
An HTTPS sprite base URL | Same |
"glyphs": "mapbox://fonts/…/{fontstack}/{range}.pbf" |
An HTTPS glyph template | Same |
accessToken in the map options |
Removed | No token concept |
"source-layer" values |
Unchanged | The tileset’s own layer names |
| Expressions, filters, paint | Unchanged | Same spec version |
The three mapbox:// lines are the whole migration for most styles. Everything under layers — which is 95% of a real style document by size — is untouched.
Production Command
# 1. Find every service dependency in the style
jq -r '
[ (.sources | to_entries[] | select(.value.url? // "" | startswith("mapbox://"))
| "source \(.key): \(.value.url)"),
(select(.sprite? // "" | startswith("mapbox://")) | "sprite: \(.sprite)"),
(select(.glyphs? // "" | startswith("mapbox://")) | "glyphs: \(.glyphs)")
] | .[]
' style-mapbox.json
# source composite: mapbox://mapbox.mapbox-streets-v8
# sprite: mapbox://sprites/mapbox/streets-v12
# glyphs: mapbox://fonts/mapbox/{fontstack}/{range}.pbf
# 2. Substitute your own hosting
jq '
.sources.composite.url = "https://tiles.example.com/v43/basemap.json"
| .sprite = "https://tiles.example.com/v43/sprite"
| .glyphs = "https://tiles.example.com/fonts/{fontstack}/{range}.pbf"
' style-mapbox.json > style-maplibre.json
# 3. Validate against the MapLibre spec
npx @maplibre/maplibre-gl-style-spec validate style-maplibre.json
And the map initialisation loses its token:
// Before
mapboxgl.accessToken = "pk.eyJ1...";
const map = new mapboxgl.Map({ container: "map", style: "mapbox://styles/mapbox/streets-v12" });
// After
const map = new maplibregl.Map({
container: "map",
style: "https://tiles.example.com/v43/style.json",
});
The Part That Is Not Mechanical
Substituting URLs is easy. Making the new tileset satisfy the style is the real work, because a Mapbox-authored style expects Mapbox’s schema — layer names like road, building, water, with Mapbox’s attribute conventions.
Two routes. Use a tileset that deliberately matches that schema — OpenMapTiles and Protomaps both publish schemas with documented layer names — and the style largely works as written. Or use your own tileset and rewrite every source-layer and every attribute reference to match it, which is the same contract-checking exercise as any schema change, just all at once.
The diff between what the style expects and what the tileset publishes is worth computing rather than discovering:
# What the style asks for
jq -r '[.layers[] | select(.["source-layer"]) | .["source-layer"]] | unique | .[]' \
style-maplibre.json | sort > /tmp/style-wants
# What the tileset publishes
pmtiles show basemap.pmtiles | jq -r '.vector_layers[].id' | sort > /tmp/tiles-have
comm -23 /tmp/style-wants /tmp/tiles-have | sed 's/^/style references a missing layer: /'
Post-Fork Divergence
MapLibre forked from Mapbox GL JS v1 and both have moved since. Features added to Mapbox after the fork have no MapLibre equivalent, and a style using them validates as unknown properties rather than failing loudly.
The ones that come up in practice are Mapbox’s 3D terrain and sky configuration, which MapLibre implements differently and with different property names; fill-extrusion extensions added post-fork; and Mapbox Standard’s lighting model, which has no counterpart. The validator flags each as an unknown property, so running it and reading the warnings is how the list for a specific style gets built — do not skip the warnings on the assumption they are cosmetic.
Interaction Effects
With sprite and glyph hosting. Removing the mapbox:// scheme means those assets become yours to produce and serve. That is the sprite and glyph pipeline, and it is usually the larger half of a migration by effort.
With attribution. The tileset changes, so the attribution must too. A migrated style still carrying Mapbox attribution is both wrong and a licensing problem in the other direction.
With the validation suite. Run the full validation workflow against the migrated style before deploying. A migration touches the two things — source URLs and asset URLs — that structural validation alone cannot check.
Performance Impact
The migration itself changes nothing about rendering; MapLibre and Mapbox GL JS v1 are the same renderer lineage. What changes is delivery, and usually for the better: a self-hosted PMTiles archive behind your own CDN removes a third-party dependency from the critical path and, on a cold viewport, typically saves the DNS and TLS setup for an extra origin.
The one regression to watch for is glyph coverage. Mapbox’s hosted glyph endpoint carries a very wide font range; a self-hosted set covering only what the style names will 404 on a label using an unexpected script, which appears as missing labels in exactly one region.
Common Mistakes
Leaving a mapbox:// URL in a nested source. The obvious ones are found by eye; a style with a dozen sources can hide one, and that source silently fails to load.
Assuming the tileset schemas match. A Mapbox-authored style against an arbitrary tileset renders almost nothing, and the cause is dozens of source-layer mismatches rather than one bug.
Ignoring validator warnings about unknown properties. They are the post-fork feature list, and each one is something that will not render.
Carrying the old attribution. Both a factual error and a licence issue.
FAQ
Is MapLibre a drop-in replacement for Mapbox GL JS?
For the v1 API, close to it: the map constructor, the layer API and the expression language match. Code written against Mapbox GL JS v2 or later may use APIs that diverged after the fork.
Can I keep using Mapbox-hosted tiles with MapLibre?
Technically yes, by substituting the resolved HTTPS TileJSON URL with a token query parameter, and it puts your token in every client’s network panel. If the goal is to leave the service, leave the tiles too.
What about Mapbox Studio styles?
Export the style JSON from Studio and treat it as the input to this migration. The Studio-specific metadata is ignored by MapLibre and can be left in place or stripped.
Does the expression syntax differ at all?
Not for anything in the v8 spec. Both implementations share the same expression evaluator lineage, so filters and data-driven properties behave identically.
Related
- MapLibre GL JSON Structure — the parent topic and the anatomy of the document being migrated.
- Sprite and Glyph Pipelines — producing the assets the
mapbox://URLs used to supply. - Style Validation Workflows — running the checks a migration most needs.
- Layer Filter Synchronization — reconciling the style’s expected schema with the tileset’s real one.