Guide

react-map-gl with MapLibre

react-map-gl wraps MapLibre GL JS in React components: the map, markers, popups, controls, sources and layers. A map component with a style URL, a marker and 3D terrain, the setup MapLibre 6 needs with Vite and Next.js, and where the key goes. Tested with react-map-gl 8.1.3, MapLibre GL JS 6.13.0, React 19.3 and Vite 8.

npm install react-map-gl maplibre-gl
import Map from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";

export default function MapView() {
  return (
    <Map
      initialViewState={{ longitude: 8.54, latitude: 47.37, zoom: 12 }}
      style={{ width: "100%", height: 480 }}
      mapStyle="https://api.overpass.ch/v1/YOUR_KEY/maps/styles/light.json"
    />
  );
}
With Vite, add the setWorkerUrl lines below, or the map stays empty.

A map with a marker, a popup and 3D terrain

This component ran in a Vite build. It shows the outdoor style tilted over the Matterhorn, with terrain, a marker and its popup:

import { useState } from "react";
import Map, { NavigationControl, Marker, Popup, Source } from "react-map-gl/maplibre";
import { setWorkerUrl } from "maplibre-gl";
import workerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url";
import "maplibre-gl/dist/maplibre-gl.css";

setWorkerUrl(workerUrl); // MapLibre 6 with Vite: once, before the first map

const KEY = import.meta.env.VITE_OVERPASS_KEY;
const MAPS = `https://api.overpass.ch/v1/${KEY}/maps`;

export default function MapView() {
  const [open, setOpen] = useState(true);
  return (
    <Map
      initialViewState={{ longitude: 7.6586, latitude: 45.9763, zoom: 12, pitch: 60, bearing: -100 }}
      style={{ width: "100%", height: 480 }}
      mapStyle={`${MAPS}/styles/outdoor.json?lang=en`}
      maxPitch={85}
      terrain={{ source: "terrain-3d", exaggeration: 1.2 }}
    >
      <Source id="terrain-3d" type="raster-dem" url={`${MAPS}/terrain.json`} />
      <NavigationControl visualizePitch />
      <Marker longitude={7.7491} latitude={46.0207} color="#d0312d" onClick={() => setOpen(true)} />
      {open && (
        <Popup longitude={7.7491} latitude={46.0207} anchor="bottom" offset={36} onClose={() => setOpen(false)}>
          Zermatt
        </Popup>
      )}
    </Map>
  );
}
  • Import from react-map-gl/maplibre. It loads maplibre-gl by itself and works with the ES modules of MapLibre 6. The package @vis.gl/react-maplibre has the same components without the Mapbox part.
  • mapStyle takes the style URL. Changing it switches the style, for example from light to dark with the theme of your app.
  • terrain refers to a raster-dem source by its id. The Source child adds that source. For 3D without code, add terrain=1 to the style URL; that style already contains a source terrain-3d.
  • For data of your own, use <Source type="geojson" data={…}> with a <Layer> child.

The worker URL

MapLibre parses tiles in a web worker. With a bundler, MapLibre 6 does not find the worker file by itself: the map stays empty and the console shows “Worker failed to load. Check that the worker URL is correct.” That is what we saw before adding the call.

  • Vite (tested): import workerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url" and setWorkerUrl(workerUrl), as above.
  • Next.js 15 and later, with Turbopack or webpack: the MapLibre documentation gives setWorkerUrl(new URL("maplibre-gl/dist/maplibre-gl-worker.mjs", import.meta.url).toString()) in a file that starts with "use client". For Next.js 14, it says to copy the worker file to public/ and pass that path.

The map needs the browser, so in Next.js put it in a client component. With server rendering, the container renders on the server and the map starts in the browser.

The key

The style URL contains your key, and the browser sees it. Put it in an environment variable so that it stays out of your repository:

# .env.local (Vite: VITE_ prefix; Next.js: NEXT_PUBLIC_ prefix)
VITE_OVERPASS_KEY=YOUR_KEY

Then create a key of the type “Maps only” in your dashboard, with your website and localhost:5173 (Vite) or localhost:3000 (Next.js) as allowed referers. Such a key works for tiles and styles only, and other websites are refused.

Tiles

react-map-gl adds nothing to the count: every vector, terrain or contour tile that MapLibre loads counts as one map tile, and styles and fonts are free. In 3D the map loads tiles up to the horizon; the 3D terrain guide has numbers. Every account includes 100'000 tiles a month.