Guide

Vector map tiles in Leaflet

Leaflet shows map images, and has no renderer for vector tiles with a style. The maplibre-gl-leaflet plugin puts a MapLibre map into Leaflet as a layer, so your Leaflet markers, popups and GeoJSON stay as they are. With script tags and with a bundler. Tested with Leaflet 1.9.4 and maplibre-gl-leaflet 0.1.4.

With script tags, MapLibre 5 and the plugin:
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.js"></script>
<script src="https://unpkg.com/@maplibre/maplibre-gl-leaflet@0.1.4/leaflet-maplibre-gl.js"></script>

<style>#map { height: 480px; }</style>
<div id="map"></div>
<script>
const map = L.map("map").setView([47.37, 8.54], 13);
L.maplibreGL({
  style: "https://api.overpass.ch/v1/YOUR_KEY/maps/styles/light.json?lang=en"
}).addTo(map);
</script>

With script tags

The plugin needs the classic build of MapLibre, which loads with a <script src> tag and defines maplibregl. MapLibre 6 comes as ES modules only, so script tags use MapLibre 5; version 5.24.0 is the last. Leaflet layers go on top of the vector map as usual:

const map = L.map("map").setView([47.37, 8.54], 13);
const base = L.maplibreGL({
  style: "https://api.overpass.ch/v1/YOUR_KEY/maps/styles/light.json?lang=en"
}).addTo(map);

L.marker([47.3769, 8.5417]).addTo(map).bindPopup("Zürich HB").openPopup();
L.geoJSON(route, { style: { color: "#d0312d", weight: 4 } }).addTo(map);

Any of our nine styles works: light, dark, white, grayscale, black, outdoor, marine, relief and driving. Add &hillshade=1 for shading. Leaflet cannot tilt the map, so terrain=1 has no use here.

With a bundler and MapLibre 6

npm install leaflet maplibre-gl @maplibre/maplibre-gl-leaflet

This ran in a Vite 8 build with MapLibre 6.13.0:

import L from "leaflet";
import "leaflet/dist/leaflet.css";
import "maplibre-gl/dist/maplibre-gl.css";
import { setWorkerUrl } from "maplibre-gl";
import workerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url";
import { maplibreGL } from "@maplibre/maplibre-gl-leaflet";
import markerIcon from "leaflet/dist/images/marker-icon.png";
import markerIcon2x from "leaflet/dist/images/marker-icon-2x.png";
import markerShadow from "leaflet/dist/images/marker-shadow.png";

setWorkerUrl(workerUrl);

// the bundler moves the marker images: tell Leaflet where they are
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({ iconUrl: markerIcon, iconRetinaUrl: markerIcon2x, shadowUrl: markerShadow });

const KEY = import.meta.env.VITE_OVERPASS_KEY;

const map = L.map("map").setView([47.37, 8.54], 13);
maplibreGL({
  style: `https://api.overpass.ch/v1/${KEY}/maps/styles/light.json?lang=en`
}).addTo(map);

L.marker([47.3769, 8.5417]).addTo(map).bindPopup("Zürich HB").openPopup();

Two lines fix the errors we met on the way:

  • Without setWorkerUrl, the map stays empty and the console shows “Worker failed to load. Check that the worker URL is correct.” MapLibre 6 needs the call with every bundler; for webpack, see the MapLibre migration guide.
  • Without the marker images, markers are invisible and the network tab shows 404 for marker-icon.png and marker-shadow.png. This is a Leaflet problem with every bundler.

The MapLibre map inside

getMaplibreMap() returns the MapLibre map, for anything Leaflet cannot do, such as a contour layer or a click on a feature of the base map:

const gl = base.getMaplibreMap();
gl.on("load", () => {
  gl.addSource("contours", { type: "vector", url: "https://api.overpass.ch/v1/YOUR_KEY/maps/contours.json" });
  gl.addLayer({ id: "contours", type: "line", source: "contours", "source-layer": "contours",
    paint: { "line-color": "rgba(124, 92, 58, 0.5)" } });
});

The zoom levels differ by one: Leaflet zoom 13 is MapLibre zoom 12, because MapLibre counts with tiles of 512 pixels. The plugin converts between them.

Limits

  • Leaflet cannot tilt or rotate the map. For 3D terrain or a rotating map, use MapLibre GL JS directly: L.marker becomes maplibregl.Marker, and GeoJSON layers become sources and layers.
  • overpass.ch has no PNG map images. For a plain image tile layer, use a raster tile service.
  • Your key is visible in the page. Create a key of the type “Maps only” in your dashboard and enter your website as an allowed referer, and localhost:5173 for the Vite dev server.
  • Tiles count as with MapLibre: one map tile per vector tile. Every account includes 100'000 a month.