Add a Hillshade Layer in MapLibre GL JS
A hillshade layer lights the terrain from one side, so mountains and valleys throw shadows and
the shape of the ground becomes visible. In MapLibre GL JS it takes two pieces: a raster-dem
source with the Maptoolkit terrain tiles, and a hillshade layer that shades it. The Maptoolkit
styles already include hillshading, so you need this for a style of your own, a relief map, or a
basemap without relief. This example builds a relief map of the Alps around Innsbruck, and the
slider changes the strength of the shading.
const API_KEY = "YOUR_API_KEY";
// A style with no basemap: a background, the terrain source and the hillshade layer.
const map = new maplibregl.Map({
container: "map",
style: {
version: 8,
sources: {
dem: {
type: "raster-dem",
tiles: [`https://tiles.maptoolkit.net/terrain/{z}/{x}/{y}.webp?api_key=${API_KEY}`],
tileSize: 256,
// The zoom levels the terrain tiles exist at; beyond 12 MapLibre overzooms.
minzoom: 5,
maxzoom: 12,
// Required: the Maptoolkit terrain tiles are Terrarium-encoded.
encoding: "terrarium",
attribution: "© <a href='https://www.maptoolkit.com' target='_blank'>Maptoolkit</a>",
},
},
layers: [
{ id: "background", type: "background", paint: { "background-color": "#f4f2ee" } },
{
id: "hillshade", type: "hillshade", source: "dem",
paint: { "hillshade-exaggeration": 0.5, "hillshade-shadow-color": "#473b24" },
},
],
},
center: [11.39085, 47.27574],
zoom: 10,
attributionControl: { compact: false },
});
map.addControl(new maplibregl.NavigationControl(), "top-right");
const slider = document.getElementById("exaggeration");
slider.addEventListener("input", () => {
document.getElementById("value").textContent = slider.value;
map.setPaintProperty("hillshade", "hillshade-exaggeration", Number(slider.value));
});<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.css" />
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.js"></script>
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
#map { width: 100%; height: 100%; }
#panel {
position: absolute; top: 12px; left: 12px; z-index: 1; width: 200px; padding: 10px 12px;
background: #fff; border-radius: 10px; box-shadow: 0 4px 20px rgba(20, 30, 60, 0.18);
font: 13px/1.4 system-ui, sans-serif; color: #1f2430;
}
#panel label { display: flex; justify-content: space-between; }
#panel input { width: 100%; accent-color: #303f7e; }
</style>
</head>
<body>
<div id="map"></div>
<div id="panel">
<label>Exaggeration <span id="value">0.5</span></label>
<input id="exaggeration" type="range" min="0" max="1" step="0.05" value="0.5" />
</div>
<script>
const API_KEY = "YOUR_API_KEY";
// A style with no basemap: a background, the terrain source and the hillshade layer.
const map = new maplibregl.Map({
container: "map",
style: {
version: 8,
sources: {
dem: {
type: "raster-dem",
tiles: [`https://tiles.maptoolkit.net/terrain/{z}/{x}/{y}.webp?api_key=${API_KEY}`],
tileSize: 256,
// The zoom levels the terrain tiles exist at; beyond 12 MapLibre overzooms.
minzoom: 5,
maxzoom: 12,
// Required: the Maptoolkit terrain tiles are Terrarium-encoded.
encoding: "terrarium",
attribution: "© <a href='https://www.maptoolkit.com' target='_blank'>Maptoolkit</a>",
},
},
layers: [
{ id: "background", type: "background", paint: { "background-color": "#f4f2ee" } },
{
id: "hillshade", type: "hillshade", source: "dem",
paint: { "hillshade-exaggeration": 0.5, "hillshade-shadow-color": "#473b24" },
},
],
},
center: [11.39085, 47.27574],
zoom: 10,
attributionControl: { compact: false },
});
map.addControl(new maplibregl.NavigationControl(), "top-right");
const slider = document.getElementById("exaggeration");
slider.addEventListener("input", () => {
document.getElementById("value").textContent = slider.value;
map.setPaintProperty("hillshade", "hillshade-exaggeration", Number(slider.value));
});
</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
raster-dem, not raster. A raster source would draw the terrain tiles as a picture.
raster-dem tells MapLibre to read each pixel as a height and compute slopes from it, which the
hillshade layer then turns into light and shadow.
encoding: "terrarium" is required. The Maptoolkit terrain tiles are Terrarium-encoded, while
MapLibre assumes Mapbox Terrain-RGB by default. The wrong encoding raises no error: MapLibre
decodes the same pixels with the wrong formula and draws plausible but wrong relief.
minzoom and maxzoom describe the tileset. The terrain tiles exist from zoom 5 to 12.
Beyond 12, MapLibre scales up the zoom 12 tiles instead of requesting more detail. A tile outside
the range returns a 404, not an empty tile.
A style of your own. The style object has only a background, the terrain source and the
hillshade layer. To shade a basemap that has no relief, add the same source and layer to it with
addSource and addLayer, passing the first symbol layer’s id to addLayer so the shadows fall
below the labels. The Maptoolkit styles don’t need it: they ship with hillshading. A style built
by hand has no attribution of its own, so the source carries it.
Exaggeration. hillshade-exaggeration runs from 0 to 1 and sets how strongly slope becomes
shadow. 0.5 reads well over a basemap. Raise it for dramatic alpine relief, lower it when roads
and labels start competing with the shading. setPaintProperty() changes it live, which is what
the slider calls. The warm shadow color reads softer than the default black.
Next steps
To light the terrain from several directions at once, so slopes facing away from the light keep their detail, see Add a Multidirectional Hillshade Layer. For the heights themselves, add contour lines, and for a 3D view, the same terrain tiles drive 3D terrain.
The same example in Maptoolkit Maps JS is Add a Hillshade Layer.