Guide

MapLibre GL JS tutorial: your first map

MapLibre GL JS draws vector maps in the browser with WebGL, from a style that names the tiles, colours, fonts and icons. A map with our tiles, markers and popups, your own data as a layer, other styles and languages, and the controls. With script tags and with Vite. Tested with MapLibre GL JS 6.16.0.

A whole page, with MapLibre from a CDN:
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@6.16.0/dist/maplibre-gl.css">
<style>#map { height: 480px; }</style>
<div id="map"></div>
<script type="module">
import * as maplibregl from "https://unpkg.com/maplibre-gl@6.16.0/dist/maplibre-gl.mjs";

const map = new maplibregl.Map({
  container: "map",
  style: "https://api.overpass.ch/v1/YOUR_KEY/maps/styles/light.json?lang=en",
  center: [8.54, 47.375], // longitude, latitude
  zoom: 13
});
map.addControl(new maplibregl.NavigationControl());
</script>
Loading the map…
The marker and the shops layer from the code below. Click the marker or a red dot, then change the style: the layer stays.

With script tags

MapLibre 6 comes as ES modules: load it in a <script type="module"> with an import from a CDN, and pin the version. The CSS file places the controls, markers and popups; without it they lose their positions. For a classic <script src> tag, use MapLibre 5, the last version with that build: maplibre-gl@5.24.0/dist/maplibre-gl.js defines the global maplibregl.

The centre is [longitude, latitude], the reverse of Leaflet. MapLibre counts zoom levels in tiles of 512 pixels, so zoom 12 in MapLibre shows the area of zoom 13 in Leaflet and OpenLayers.

With npm and Vite

npm install maplibre-gl

This ran in a Vite 8.3 build:

import "maplibre-gl/dist/maplibre-gl.css";
import { Map, NavigationControl, setWorkerUrl } from "maplibre-gl";
import workerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url";

setWorkerUrl(workerUrl);

const KEY = import.meta.env.VITE_OVERPASS_KEY;

const map = new Map({
  container: "map",
  style: `https://api.overpass.ch/v1/${KEY}/maps/styles/light.json?lang=en`,
  center: [8.54, 47.375],
  zoom: 13,
});
map.addControl(new NavigationControl());
  • MapLibre reads the tiles in a web worker. With a bundler, MapLibre 6 does not find the worker file by itself: without setWorkerUrl the map stays empty and the console shows “Worker failed to load. Check that the worker URL is correct.” For webpack, see the MapLibre migration guide.
  • Vite reads VITE_OVERPASS_KEY from a .env file next to package.json. The key still ends up in the JavaScript of the page, so limit it to your website (below).

Markers and popups

new maplibregl.Marker({ color: "#d0312d" })
  .setLngLat([8.5402, 47.3782])
  .setPopup(new maplibregl.Popup({ offset: 25 }).setText("Zürich HB"))
  .addTo(map);

A marker is an HTML element above the map: a click opens its popup, and it stays when the style changes. For hundreds of points, draw them as a layer instead, as in the next section.

Your data as a layer

function addShops() {
  map.addSource("shops", { type: "geojson", data: "shops.geojson" });
  map.addLayer({
    id: "shops",
    type: "circle",
    source: "shops",
    paint: { "circle-radius": 7, "circle-color": "#d0312d", "circle-stroke-width": 2, "circle-stroke-color": "#fff" }
  });
}
map.on("style.load", addShops);

map.on("click", "shops", (e) => {
  new maplibregl.Popup().setLngLat(e.lngLat).setText(e.features[0].properties.name).addTo(map);
});
map.on("mouseenter", "shops", () => { map.getCanvas().style.cursor = "pointer"; });
map.on("mouseleave", "shops", () => { map.getCanvas().style.cursor = ""; });
  • data takes a URL or a GeoJSON object, with coordinates in longitude and latitude.
  • Add sources and layers in style.load, not in load. load fires once, and map.setStyle removes your layers with the old style; we checked that the layer is gone after a change from light to dark. style.load fires for every style, so the layer comes back.
  • map.on("click", "shops", …) fires only for features of that layer, and e.features holds them.

Other styles, languages and dark mode

const style = (name) => `https://api.overpass.ch/v1/YOUR_KEY/maps/styles/${name}.json?lang=de`;
const dark = matchMedia("(prefers-color-scheme: dark)").matches;

const map = new maplibregl.Map({
  container: "map",
  style: style(dark ? "dark" : "light"),
  center: [8.54, 47.375],
  zoom: 13
});

// later, for example from a button:
map.setStyle(style("outdoor"));

There are nine styles: light, dark, white, grayscale and black by Protomaps, and our own outdoor, marine, relief and driving, shown on the map tiles page. lang sets the language of the labels; the 41 languages are in the documentation.

Controls

map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }));
map.addControl(new maplibregl.GeolocateControl({ trackUserLocation: true }));
map.addControl(new maplibregl.ScaleControl({ unit: "metric" }));
map.addControl(new maplibregl.FullscreenControl());

The credits in the corner come with the map. Keep them: the OpenStreetMap licence requires the credit.

Hillshading, 3D terrain and contour lines

const map = new maplibregl.Map({
  container: "map",
  style: "https://api.overpass.ch/v1/YOUR_KEY/maps/styles/outdoor.json?terrain=1",
  center: [7.6586, 45.9763], // the Matterhorn
  zoom: 12.5,
  pitch: 70,
  maxPitch: 85
});

terrain=1 adds 3D terrain to any style, hillshade=1 shading. The details are in the guides on 3D terrain, hillshading and contour lines; whole maps in a hiking map and a nautical map.

Your key and what counts

  • 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.
  • Every vector tile and terrain tile counts as one map tile; styles, fonts, icons and tiles the browser already has are free. Every account includes 100'000 map tiles a month.

Other libraries

The same map in Leaflet and OpenLayers, as a React component with react-map-gl, and the changes for code written for Mapbox GL JS. How the three libraries differ: MapLibre vs Leaflet vs OpenLayers.