MapSnippets logoMapSnippets logo

3D Building Extrusions with MapLibre GL JS

MapLibre GL JS

Add 3D extruded buildings from MapTiler vector tiles using fill-extrusion layers.

progress_activityLoading map...
const map = new maplibregl.Map({
  container: 'map',
  style: 'https://api.maptiler.com/maps/streets-v4/style.json?key=YOUR_MAPTILER_KEY',
  center: [14.4205, 50.0875],
  zoom: 16,
  pitch: 55,
  bearing: -20,
  canvasContextAttributes: { antialias: true }
});
map.addControl(new maplibregl.NavigationControl());
map.on('load', () => {
  const style = map.getStyle();
  const vectorSrc = Object.keys(style.sources).find(k => style.sources[k].type === 'vector');
  if (!vectorSrc) return;
  // Insert the 3D layer BEFORE the first label layer so labels stay on top.
  const labelLayerId = style.layers.find(l => l.type === 'symbol' && l.layout && l.layout['text-field'])?.id;
  map.addLayer({
    id: '3d-buildings',
    source: vectorSrc,
    'source-layer': 'building',
    type: 'fill-extrusion',
    minzoom: 14,
    filter: ['!=', ['get', 'hide_3d'], true],
    paint: {
      'fill-extrusion-color': '#aaa',
      'fill-extrusion-height': [
        'interpolate', ['linear'], ['zoom'],
        14, 0,
        16, ['coalesce', ['get', 'render_height'], 0]
      ],
      'fill-extrusion-base': ['coalesce', ['get', 'render_min_height'], 0],
      'fill-extrusion-opacity': 0.7
    }
  }, labelLayerId);
});

How it works

MapTiler vector styles include building footprints with height data in the building source-layer. The fill-extrusion layer type renders these polygons as 3D shapes using render_height and render_min_height properties from the vector tiles.

The code detects the vector source name at runtime instead of hardcoding it. MapTiler styles may use maptiler_planet, openmaptiles, or other source names depending on the style version.

The layer is inserted before the first label layer so that road/POI labels stay rendered on top of the buildings. The filter ['!=', ['get', 'hide_3d'], true] drops features that the tiles explicitly mark as “not for 3D rendering”: without it, MapLibre 5.x throws a TypeError inside loadTile when processing tiles with mixed 2D/3D hints.

['coalesce', ['get', 'render_height'], 0] also protects against features where render_height is missing or null (returns 0 instead of null, which the shader can handle).

Set pitch above 0 in the constructor to view buildings from an angle. Without pitch, extrusions are invisible because you are looking straight down.

Key APIs

API Description Docs
fill-extrusion layer 3D extruded polygon rendering maplibre.org docs
fill-extrusion-height Top height of extrusion in meters maplibre.org docs
fill-extrusion-base Base height of extrusion in meters maplibre.org docs

FAQ

Why do I need to detect the vector source name?

Different MapTiler style versions use different source names. Hardcoding openmaptiles would break with newer styles that use maptiler_planet.

Can I color buildings based on their height?

Yes. Use an interpolate expression on render_height to map heights to colors, e.g., taller buildings get darker shading.

Common mistakes

  • Hardcoding the vector source name instead of detecting it at runtime
  • Forgetting to set pitch > 0: extrusions are invisible from directly above
  • Adding the layer before the map loads: always use map.on('load', ...)
  • Passing raw ['get', 'render_height'] without coalesce: features with null height crash loadTile in MapLibre 5.x
  • Adding the layer without the hide_3d filter and without a beforeId: labels and icons end up buried under buildings, and features the tiles marked as not-for-3D still get extruded
auto_awesome

Build maps using MapLibre GL JS with AI

Generate code like this 3d building extrusions with maplibre gl js example using the MapLibre GL JS skill in your IDE.