Skip to content

Add 3D Terrain in MapLibre GL JS

Map with 3D Terrain

Adds a 3D terrain effect using the Terrain RGB tileset. Set pitch to tilt the map, then add the terrain source and call setTerrain:

const map = new maplibregl.Map({
      container: 'map',
      style: 'https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=YOUR_API_KEY',
      center: [12.805988, 47.310897],
      zoom: 12,
      pitch: 65,
    });

    map.on('load', () => {
      map.addSource('terrain', {
        type: 'raster-dem',
        tiles: ['https://tiles.maptoolkit.net/terrain/{z}/{x}/{y}.webp?api_key=YOUR_API_KEY'],
        tileSize: 256,
        maxzoom: 12,
        minzoom: 5,
        encoding: "terrarium"
      });

      map.setTerrain({ source: 'terrain' });
    });

    map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }));
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <script src="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.css" />
  <style>
    html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
    #map { width: 100%; height: 100%; }
  </style>
</head>
<body>
  <div id="map" class="map"></div>
  <script>
    const map = new maplibregl.Map({
      container: 'map',
      style: 'https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=YOUR_API_KEY',
      center: [12.805988, 47.310897],
      zoom: 12,
      pitch: 65,
    });

    map.on('load', () => {
      map.addSource('terrain', {
        type: 'raster-dem',
        tiles: ['https://tiles.maptoolkit.net/terrain/{z}/{x}/{y}.webp?api_key=YOUR_API_KEY'],
        tileSize: 256,
        maxzoom: 12,
        minzoom: 5,
        encoding: "terrarium"
      });

      map.setTerrain({ source: 'terrain' });
    });

    map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }));
  </script>
</body>
</html>

Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.

Use the Maptoolkit Connector. Create an html file of an interactive map using vector tiles and the MapLibre GL JS. Use zoom level 11, center it around Zell am See, and add 3d terrain. The default view should display 3d terrain. Maptoolkit

How it works

Terrain is two separate things: a source that carries elevation, and a camera angle that makes it visible.

The source is raster-dem, not raster. A raster source would paint the tiles as a picture; raster-dem tells the renderer to read each pixel as a height value.

encoding: "terrarium" is required. The tiles are Terrarium-encoded even though the path says terrain, and passing the wrong encoding produces plausible but incorrect relief rather than an error.

setTerrain runs inside map.on('load') because it attaches to the loaded style. Calling it at construction time throws.

pitch: 65 is what makes the relief visible; looking straight down hides it entirely. visualizePitch: true adds the tilt indicator to the navigation control.

minzoom: 5 and maxzoom: 12 describe the tileset. Zoom 12 is its maximum, so the renderer overzooms past that point rather than fetching more detail.

Next steps

The obvious next move is tuning. exaggeration on setTerrain scales the relief, and anything above 1 makes mountains read better while misrepresenting the ground, which matters if the map is used for planning rather than illustration.

After that, terrain pairs with things rather than standing alone. Contour lines from the same DEM give measured heights over the shading, and a route drawn on tilted terrain is far more legible than the same route flat. The Routing API returns that geometry, and the Elevation API turns it into a climb profile.