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.

The four substitutions a migration actually makesSource URLs, sprite URL, glyph template and the access token are replaced or removed, while the layers array carries across unchanged.MIGRATIONsourcesmapbox:// to https://spriteyour own sheetglyphsyour own rangestokenremovedlayersunchangedthe bulk of thedocument
The layers array is the expensive part of a style and it does not change. That is why this migration is usually an afternoon rather than a project.

Production Command

bash
# 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:

javascript
// 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:

bash
# 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: /'
What carries across, what changes, and what has no equivalentSix categories of style content classified by whether they migrate unchanged, need a substitution, or have no MapLibre equivalent.COMPATIBILITYstatusactionLayers, filters,expressionsUnchangedNonePaint and layoutpropertiesUnchangedNoneSource URLsmapbox:// unsupportedSubstitute HTTPSSprite and glyphsmapbox:// unsupportedHost your ownAccess tokenNo conceptRemovePost-fork Mapbox featuresNo equivalentRedesign or drop
Only the bottom row needs a design decision. Everything above it is either untouched or a URL substitution.
What a migration actually costs, by taskFour migration tasks measured in relative effort: URL substitution, hosting sprites and glyphs, reconciling the tileset schema, and replacing post-fork features.WHERE THE EFFORT GOESSubstituting mapbox:// URLsminutesHosting sprites and glyphsa dayReconciling the tileset schemadays, if schemas differReplacing post-fork featuresper feature
The URL substitution is minutes. Hosting the assets that the mapbox:// scheme used to supply is the bulk of a real migration.

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.