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.
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.