Skip to content

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.

Use the Maptoolkit Connector. Create a MapLibre GL JS relief map around Innsbruck from a style with only a background, the Maptoolkit terrain tiles as a terrarium-encoded raster-dem source and a hillshade layer, with a slider for the hillshade exaggeration.

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.