Skip to content
Go back

MapLibre GL: Mapbox Replacement

By SumGuy 10 min read
MapLibre GL: Mapbox Replacement
Contents

The Fork That Fixed the Map

In December 2020, Mapbox dropped a changelog entry that sent the open-source community into a quiet rage. mapbox-gl-js version 2.0 was no longer BSD licensed. The new terms were the proprietary Mapbox Terms of Service, the software world’s way of saying “open-ish, but not really, and we can change the terms whenever we want.” Commercial use of the renderer required a Mapbox account and API token.

The timing was either brave or tone-deaf depending on where you stood. mapbox-gl-js was one of the most forked and integrated web mapping libraries on the planet. Millions of lines of third-party code assumed it was free. The previous version (1.x) was still BSD-3-Clause, but Mapbox immediately wound down maintenance on it, only critical security and browser-compat fixes, for a limited time.

The community’s response took about forty-eight hours. A group of developers from Carto, AWS, Microsoft, MapTiler, and a pile of individual contributors grabbed the last MIT-licensed commit and kept walking. The fork became MapLibre GL JS. It now has its own nonprofit governance (the MapLibre Organization), a consistent release cadence, and, most importantly, it’s genuinely better than the pre-fork library in almost every way that counts.

Full example: Minimal HTML demo + style.json at github.com/KingPin/sumguy-examples/tree/main/self-hosting/maplibre-gl-mapbox-replacement

What “Proprietary” Actually Means Here (and Why You Should Care)

The v2 terms are not open source. The Open Source Initiative recognizes nothing about the Mapbox Terms of Service. It’s a plain vendor license bolted onto the renderer. (Worth being precise: this is not the Business Source License you’ve seen from the likes of HashiCorp or Sentry, where the terms auto-convert to a real open-source license after a few years. Mapbox’s TOS has no such expiry clause; it’s just proprietary.)

For the indie dev or home labber, the concrete effect is this: you cannot use mapbox-gl-js v2+ without an API token, and you cannot get an API token without being on a usage plan that Mapbox controls. Render a map? That’s a map load. They count them. At small scale the free tier covers it. At any meaningful scale you’re paying, and the pricing is high enough to feel like a tax on shipping something people actually use.

MapLibre has none of this. BSD-3-Clause. Render as many maps as you want. No token, no account, no meter ticking.

The reason forking a WebGL renderer is technically interesting isn’t just the license; it’s the complexity. mapbox-gl-js is not a convenience wrapper around a few Leaflet calls. It’s a full vector tile renderer pipeline: tile fetching, parsing, layout, symbol placement, collision detection, camera math, WebGL shader management. Maintaining that fork and continuing to improve it is a non-trivial act of engineering solidarity.

Drop-In Replacement: It’s Usually a Find-and-Replace

The single best thing about MapLibre GL JS is the API compatibility with mapbox-gl-js v1. If your project was on v1, migration is frequently this:

index.html
<!-- before -->
<script src="https://api.mapbox.com/mapbox-gl-js/v1.13.3/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v1.13.3/mapbox-gl.css" rel="stylesheet" />
<!-- after -->
<script src="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css" rel="stylesheet" />

And in your JavaScript:

// before
const map = new mapboxgl.Map({ ... });
// after
const map = new maplibregl.Map({ ... });

That’s often the entire migration. No API changes, no event renames, no control class reshuffles. The Map, Marker, Popup, LngLat, NavigationControl, GeolocateControl are all there, same signatures, same behavior. Third-party plugins that target v1’s API surface generally just work.

If you were on Mapbox v2+, the migration has a few more considerations, mainly around styles that reference mapbox:// tile sources, which you’ll need to swap out for a self-hosted or third-party source. But the renderer itself is a drop-in at the JS level.

Here’s a complete minimal HTML page wiring up a MapLibre map against a self-hosted tile server:

index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MapLibre GL Demo</title>
<script src="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css" rel="stylesheet" />
<style>
html, body, #map { margin: 0; padding: 0; height: 100%; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
const map = new maplibregl.Map({
container: 'map',
style: '/style.json', // your self-hosted style spec
center: [-87.6298, 41.8781], // Chicago
zoom: 11,
});
map.addControl(new maplibregl.NavigationControl(), 'top-right');
map.addControl(new maplibregl.ScaleControl(), 'bottom-left');
</script>
</body>
</html>

No Mapbox token. No phone-home. It just works.

The Style Spec: Same JSON, No Surprises

MapLibre uses the same Mapbox GL style specification that existed before the fork. Same JSON structure, same expression syntax, same layer types (fill, line, symbol, circle, raster, heatmap, fill-extrusion, background), same paint and layout property names. If you’ve written a Mapbox style, you already know how to write a MapLibre style.

Here’s a stripped-down style.json pointing at a self-hosted tile source:

style.json
{
"version": 8,
"name": "My Self-Hosted Style",
"sources": {
"openmaptiles": {
"type": "vector",
"tiles": ["http://localhost:8080/data/v3/{z}/{x}/{y}.pbf"],
"maxzoom": 14
}
},
"layers": [
{
"id": "background",
"type": "background",
"paint": { "background-color": "#1a1a2e" }
},
{
"id": "water",
"type": "fill",
"source": "openmaptiles",
"source-layer": "water",
"paint": { "fill-color": "#0f3460" }
},
{
"id": "roads",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"paint": {
"line-color": "#e94560",
"line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.5, 14, 3]
}
}
]
}

The ["interpolate", ["linear"], ["zoom"], ...] expression is identical to what you’d write in a Mapbox style. Style editors like Maputnik export standard GL JSON that MapLibre consumes without modification.

One genuinely new capability MapLibre added post-fork: PMTiles support. PMTiles is a single-file tile archive format built by Protomaps. Instead of running a tile server, you can drop a .pmtiles file on any static host or R2/S3 bucket and point MapLibre directly at it using the pmtiles:// protocol (via the pmtiles plugin). Zero infrastructure, one file. For personal projects and internal tools, this is an good answer to “I just want tiles.”

Pairing With Self-Hosted Tiles

MapLibre is just the renderer. It needs tiles to render. Your options in the self-hosted world are solid:

Tileserver-GL is the standard approach for serving MBTiles or directories of pre-generated vector tiles. Point your style.json at it:

Terminal window
# Verify a Tileserver-GL tile endpoint is alive
curl "http://localhost:8080/data/v3/11/1097/754.pbf" \
-o /dev/null -w "%{http_code} %{size_download}b\n"

You should get a 200 and a non-zero byte count for any tile that exists in your dataset. A 204 means “empty tile”, valid for water-only or empty areas. A 404 means the tile source name or zoom range is wrong.

PMTiles on Cloudflare R2 or S3 is the low-ops approach. Download a planet or regional PMTiles file from Protomaps, upload it, and point MapLibre at it. The pmtiles library handles range request byte fetching client-side. No tile server process to maintain.

OpenMapTiles / OpenFreeMap gives you hosted open tiles with a permissive use policy if you need something working before your self-hosted stack is up. Point the style’s source at their tile endpoint, own the renderer and style yourself.

The key insight is that “self-hosted maps” doesn’t have to mean “run everything yourself.” MapLibre as the renderer, PMTiles on cheap object storage, and a style JSON you control is a perfectly reasonable production setup.

Plugins: The Ecosystem Kept Pace

The plugin ecosystem followed the fork cleanly because most plugins target the v1 API surface, which MapLibre preserves exactly.

Clustering: Built into MapLibre’s source options. Set cluster: true on a GeoJSON source, style your cluster circles and count labels, done. No external library needed.

map.addSource('points', {
type: 'geojson',
data: '/api/points.geojson',
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50,
});

Drawing tools: @mapbox/mapbox-gl-draw works against MapLibre with a minor rename shim. The community also maintains maplibre-gl-draw as a proper fork. Polygon, line, and point editing with snapping, all the usual stuff.

Geocoder UI: maplibre-gl-geocoder is the standard UI widget. It talks to any geocoding API, so pointing it at a self-hosted Nominatim or Photon instance is a direct config change:

const geocoder = new MaplibreGeocoder(
{
forwardGeocode: async (config) => {
const res = await fetch(
`http://localhost:8080/search?q=${config.query}&format=geojson&limit=5`
);
const data = await res.json();
return { features: data.features };
},
},
{ maplibregl }
);
map.addControl(geocoder);

That’s a Nominatim search box with zero third-party geocoding calls. Combine it with the tiles setup above and you have a fully self-hosted map stack: tiles, renderer, geocoder, no external dependencies.

Performance: Where It Stands

MapLibre’s WebGL renderer is fast. On modern desktop hardware it handles dense city tiles, thousands of GeoJSON points, and animated heatmaps without breaking a sweat. The WebGL 2 upgrade that landed post-fork gave it a meaningful headroom increase over the original v1 codebase.

Mobile is where you want to think. WebGL performance is device-dependent in ways desktop hides. A mid-range Android phone from 2022 can stutter on a style with many symbol layers (street labels, POI icons) at high zoom. A few practical notes:

The short version: MapLibre is not a liability. It’s fast enough for production map applications, and in most benchmarks it’s within noise of what Mapbox delivers at the renderer level.

Everyone Outside Big Mapbox Contracts Should Default to MapLibre

Mapbox makes good products. If your company has a Mapbox enterprise contract and the engineering resources to stay current on their platform, stay; the lock-in is already real.

If you’re anyone else (indie dev, home labber, startup, agency building client maps, open-source project), MapLibre is the correct default. Not the “scrappy open-source alternative.” The default.

The library is mature, actively maintained, and has a governance structure that can’t be unilaterally changed by one company’s board. The API you learn works against self-hosted tiles, third-party providers, and cloud services interchangeably. The community has shipped features (WebGL 2, better terrain support, improved performance on low-power devices) that Mapbox hasn’t shipped to the open ecosystem in the same period.

The migration cost, if you’re on v1, is usually a find-and-replace and an afternoon. If you’re starting fresh, there’s no reason to start with Mapbox at all.

Your data stays yours. Your tile stack stays yours. The map renders the same.



Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Synthetic Browser Monitoring with Playwright + Grafana

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts