In this blog post, we explore how to use MapLibre GL JS to embed interactive maps into web applications. MapLibre GL JS is an open-source library for rendering maps in the browser. The library supports both raster and vector tiles, and it works with a wide variety of map styles and data sources.
For this post, I will use maps from Swisstopo, the Swiss Federal Office of Topography. The maps they provide only cover Switzerland, but they are free to use without an API key and are available in a wide variety of styles and data sources, which makes them ideal for demonstrating the capabilities of MapLibre GL JS. The techniques shown in this post can be applied to any map style and data source that is compatible with MapLibre GL JS, including self-hosted tile services.
You can find the full source code for MapLibre GL JS on GitHub and the documentation on the MapLibre GL JS website.
Raster vs. Vector Tiles ¶
Before we look at the examples, here is a quick note about the two main map formats that MapLibre GL JS supports: raster tiles and vector tiles.
Raster tiles are pre-rendered images, usually in PNG or JPEG format. They are simple to serve and display, and they are a good fit for imagery, scanned maps, or overlays that already exist as images. The downside is that they are less flexible. When you zoom in, the map either becomes pixelated or blurry, or the client has to fetch a different set of tiles for the new zoom level. You also cannot restyle raster tiles in the browser because they are just images.
Vector tiles, on the other hand, contain the geographic features and attributes needed for rendering. The rendering happens on the client side, which allows for much more flexibility. Zooming is smoother because the features can be rendered at any scale, and styles can be changed dynamically without needing a new set of pre-rendered images. The tradeoff is that rendering vector tiles is more complex and requires more processing power on the client side.
MapLibre GL JS supports both formats. Often you do not have a choice because your data provider only offers one or the other. If both formats are available, the best option depends on the use case. If you want a highly interactive map with custom styling, vector tiles are usually the better choice. If you just want to display a static map or an overlay that already exists in raster form, raster tiles may be simpler.
Getting started ¶
Let's start with a simple vector tile example. First, include the MapLibre GL JS CSS and JavaScript in your HTML document.
<link href="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css" rel="stylesheet"/>
<link href="basic-vector.css" rel="stylesheet"/>
<script src="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js"></script>
Then, in the body of the document, add a container for the map. This example uses a <div> with an ID of map.
The application then creates a new maplibregl.Map instance, passing in the container ID, the style URL for the basemap, and the initial
center and zoom level. The style URL points to a hosted style JSON document that defines how the map should look and where to get the
tile data from. The center is defined as longitude and latitude coordinates, and zoom controls the initial zoom level.
With addControl(), we can add interactive controls to the map, such as zoom buttons and a scale bar.
<div id="map"></div>
<script>
const map = new maplibregl.Map({
container: 'map',
style: 'https://vectortiles.geo.admin.ch/styles/ch.swisstopo.basemap.vt/style.json',
center: [8.2312, 46.8182],
zoom: 7,
});
map.addControl(new maplibregl.NavigationControl());
map.addControl(new maplibregl.ScaleControl({
maxWidth: 100,
unit: 'metric'
}));
</script>
This CSS rule gives the map container a height and a border:
#map {
height: 600px;
border: 1px solid #ddd;
}
With just a few lines of code, you have an interactive map on the screen.
Swisstopo also provides the same map as raster tiles. The only difference from the vector tiles example is the code that initializes the map.
The sources section defines where to get the tile data from. In this case, it is a raster source that points to the Swisstopo WMTS endpoint. The URL template includes placeholders for the zoom level ({z}) and tile coordinates ({x} and {y}), which MapLibre GL JS fills in when requesting tiles.
The layers section defines how to render the data from the source. Here we add a single layer of type raster that uses the swisstopo-raster source. The minzoom and maxzoom properties specify the zoom levels at which this layer should be visible.
center and zoom are the same as in the vector tiles example, so the map will start centered on Switzerland at zoom level 7.
const map = new maplibregl.Map({
container: 'map',
style: {
"version": 8,
"sources": {
"swisstopo-raster": {
"type": "raster",
"tiles": [
"https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg"
],
"tileSize": 256,
"attribution": "© swisstopo"
}
},
"layers": [
{
"id": "swisstopo-raster-layer",
"type": "raster",
"source": "swisstopo-raster",
"minzoom": 0,
"maxzoom": 18
}
]
},
center: [8.2312, 46.8182],
zoom: 7,
});
Controls ¶
In the previous examples, we added a navigation control and a scale control to the map. These are built-in controls that MapLibre GL JS provides for common interactions.
MapLibre GL JS offers a variety of controls that you can add to your map. Some of the most commonly used controls include:
NavigationControl: provides zoom buttons and a compass for resetting the orientation.ScaleControl: shows a scale bar that indicates the distance on the map.FullscreenControl: allows the user to toggle fullscreen mode for the map.GeolocateControl: provides a button to center the map on the user's current location.
Check the MapLibre GL JS API documentation for a full list of available controls and how to use them.
Markers and popups ¶
A common use case is to add markers to the map to represent points of interest and popups to show additional information when a marker is clicked.
In this example, the application adds markers for the five highest mountain peaks in Switzerland. Each marker has a popup that shows some information about the mountain.
When creating a popup, you can customize its content using HTML, and you can also configure its behavior with several options. offset controls the
position of the popup relative to the marker, closeButton adds a close button to the popup, and closeOnClick determines whether the popup should close when the user clicks outside of it.
const popupContent = `
<div class="popup-title">${mountain.name}</div>
<div class="popup-info">
<strong>Elevation:</strong> ${mountain.elevation.toLocaleString()} m<br>
<strong>Mountain Range:</strong> ${mountain.range}<br>
<strong>First Ascent:</strong> ${mountain.firstAscent}<br><br>
${mountain.description}
</div>
`;
const popup = new maplibregl.Popup({
offset: 25,
closeButton: true,
closeOnClick: false
}).setHTML(popupContent);
When creating a marker, you can specify a custom HTML element to use as the marker icon. In this example, the marker is a div with the class mountain-marker. The CSS styles this div to look like a red circle with a white border.
The anchor option specifies how the marker is positioned relative to its coordinates. In this case, anchor: 'center' means that the center of the marker
will be placed at the specified coordinates. The code then sets the marker's position with setLngLat() and attaches the popup with setPopup().
Finally, addTo(map) adds the marker to the map.
const markerElement = document.createElement('div');
markerElement.className = 'mountain-marker';
markerElement.title = mountain.name;
new maplibregl.Marker({
element: markerElement,
anchor: 'center'
})
.setLngLat(mountain.coordinates)
.setPopup(popup)
.addTo(map);
});
MapLibre GL JS handles the interactivity, so when the user clicks on the marker, the popup will automatically open and display the content defined in popupContent.
A common requirement when showing multiple markers is to automatically zoom and pan the map so that all markers are visible. The following code
demonstrates one way to do that. First, it loops through all the marker coordinates and extends a LngLatBounds object to include each one.
The result is that bounds represents the smallest rectangle that contains all the marker coordinates. Then fitBounds() adjusts the map's
viewport to fit those bounds, with some padding and a maximum zoom level.
const bounds = new maplibregl.LngLatBounds();
mountains.forEach(mountain => {
bounds.extend(mountain.coordinates);
});
map.fitBounds(bounds, {
padding: 50,
maxZoom: 10
});
To remove markers from the map, you can call the remove() method on the marker instance. In this example, we do not keep track of the marker
instances because we do not need to remove them. In an application where markers may need to be removed later, you would store the marker instances
in a variable so that you can call remove() on them when needed.
function removeMarkers() {
markers.forEach(marker => marker.remove());
markers = [];
}
GeoJSON ¶
GeoJSON is a JSON-based format for geographic data. It can represent points, lines, polygons, and collections of features together with arbitrary attributes. Because it is plain JSON, it is easy to generate on the server, fetch from an API, or define directly in JavaScript.
MapLibre GL JS can use GeoJSON directly as a data source.
Here is an example that defines a single polygon with GeoJSON.
const geojson = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {
name: 'Sample area'
},
geometry: {
type: 'Polygon',
coordinates: [[
[7.95, 46.75],
[8.35, 46.75],
[8.35, 47.0],
[7.95, 47.0],
[7.95, 46.75]
]]
}
}
]
};
To display the data, add it as a source after the map has finished loading. The source is given a name, and the data property points to the GeoJSON object.
map.on('load', () => {
map.addSource('sample-area', {
type: 'geojson',
data: geojson
});
Once the source exists, one or more layers can render it. This example adds two layers that both read from the same GeoJSON source: a fill
layer for the polygon interior and a line layer for the outline.
map.addLayer({
id: 'sample-area-fill',
type: 'fill',
source: 'sample-area',
paint: {
'fill-color': '#1f78b4',
'fill-opacity': 0.35
}
});
map.addLayer({
id: 'sample-area-outline',
type: 'line',
source: 'sample-area',
paint: {
'line-color': '#0b3c5d',
'line-width': 2
}
});
The same approach also works for point and line data. For example, a GeoJSON source can be rendered with circle, symbol, or line layers
depending on the geometry type and the visual result you want.
WMS overlays ¶
WMS stands for Web Map Service, and it is a standard protocol for serving georeferenced map images over the web. MapLibre GL JS can display WMS layers as raster sources, which allows you to overlay additional data on top of your basemap.
Swisstopo provides many WMS layers that can be added to a MapLibre GL JS map as overlays.
This example starts with the same vector tiles as before.
const map = new maplibregl.Map({
container: 'map',
style: 'https://vectortiles.geo.admin.ch/styles/ch.swisstopo.basemap.vt/style.json',
center: [8.2312, 46.8182],
zoom: 7,
});
To add a WMS layer, create a new source of type raster and specify the WMS GetMap URL template in the tiles array. The URL includes parameters
that define the WMS request, such as the service type, version, layers to include, styles, coordinate reference system, tile size, format, transparency, and bounding box. MapLibre GL JS fills in {bbox-epsg-3857} for each requested tile so the WMS server can render the correct image.
Then you add a new layer that uses this source. The layer is of type raster, and you can set its layout and paint properties to control how it is
displayed on the map. The raster-opacity paint property is set to 0.6 to make the overlay semi-transparent. visibility controls whether the layer is visible.
if (!map.getSource('coverage-5g')) {
map.addSource('coverage-5g', {
type: 'raster',
tiles: ['https://wms.geo.admin.ch/?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&LAYERS=ch.bakom.mobilnetz-5g&STYLES=&CRS=EPSG:3857&WIDTH=256&HEIGHT=256&FORMAT=image/png&TRANSPARENT=true&BBOX={bbox-epsg-3857}'],
tileSize: 256
});
map.addLayer({
id: 'coverage-5g-layer',
type: 'raster',
source: 'coverage-5g',
layout: {
visibility: coverage5GVisible ? 'visible' : 'none'
},
paint: {
'raster-opacity': 0.6
}
});
}
In this example, the user can enable or disable the layer with a checkbox, which toggles the visibility layout property between visible and none. With setLayoutProperty(), you can change layout properties on an existing layer. getLayer() checks
whether the layer exists before trying to change its visibility.
toggle5G.addEventListener('change', function () {
coverage5GVisible = this.checked;
if (map.getLayer('coverage-5g-layer')) {
map.setLayoutProperty('coverage-5g-layer', 'visibility', coverage5GVisible ? 'visible' : 'none');
}
});
MapLibre GL JS also provides methods to remove sources and layers completely with removeSource() and removeLayer().
This is useful to free up resources when a layer is no longer needed.
function removeOfficialBaseStations() {
if (map.getLayer('official-base-stations-layer')) {
map.removeLayer('official-base-stations-layer');
}
if (map.getSource('official-base-stations')) {
map.removeSource('official-base-stations');
}
}
Switching map styles at runtime ¶
Map providers often offer the same data in multiple styles. MapLibre GL JS can switch between different
styles at runtime with the setStyle() method.
In this example, the user can choose between four different styles. When the user selects a different style from the dropdown, the change event listener calls setStyle() with the new style URL.
<div class="controls">
<label for="styleSelect">Choose map style:</label>
<select id="styleSelect">
<option value="https://vectortiles.geo.admin.ch/styles/ch.swisstopo.basemap.vt/style.json">Basemap</option>
<option value="https://vectortiles.geo.admin.ch/styles/ch.swisstopo.lightbasemap.vt/style.json">Light Basemap
</option>
<option value="https://vectortiles.geo.admin.ch/styles/ch.swisstopo.imagerybasemap.vt/style.json">Imagery Basemap
</option>
<option value="https://vectortiles.geo.admin.ch/styles/ch.swisstopo.basemap-winter.vt/style.json">Winter Basemap
</option>
</select>
</div>
<div id="map"></div>
<script>
const styleSelect = document.getElementById('styleSelect');
const map = new maplibregl.Map({
container: 'map',
style: styleSelect.value,
center: [8.2312, 46.8182],
zoom: 7,
});
map.addControl(new maplibregl.NavigationControl());
map.addControl(new maplibregl.ScaleControl({
maxWidth: 100,
unit: 'metric'
}));
styleSelect.addEventListener('change', function () {
map.setStyle(this.value);
});
map.on('styledata', () => {
console.log('Map style loaded successfully');
});
</script>
The styledata event fires when style data is loaded or updated. This is useful if you need to reapply custom sources or layers after calling setStyle(), because changing the style replaces the current style definition.
Plugins ¶
MapLibre GL JS has a plugin ecosystem that allows developers to extend the functionality of the library. The plugin page lists a variety of plugins that you can use to add new features to your map.
One example is mapbox-gl-draw, which adds interactive drawing and editing tools for points, lines, and polygons. The plugin was originally written for Mapbox GL JS, but it also works with MapLibre GL JS.
To include the plugin, add the CSS and JavaScript files to your HTML document.
<link href="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css" rel="stylesheet"/>
<link href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.5.1/mapbox-gl-draw.css" rel="stylesheet" type="text/css"/>
<link href="draw.css" rel="stylesheet"/>
<script src="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js"></script>
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.5.1/mapbox-gl-draw.js"></script>
Then you can create a new MapboxDraw instance and add it to the map as a control. The plugin provides its own set of controls for drawing and editing features on the map.
const draw = new MapboxDraw({
displayControlsDefault: false,
controls: {
polygon: true,
trash: true,
},
defaultMode: 'simple_select',
styles: drawStyles,
});
map.addControl(draw, 'top-right');
Angular integration ¶
In the last section of this post, we will look at how to integrate MapLibre GL JS into an Angular application. For this example, we will use the Angular
wrapper ngx-maplibre-gl.
Install ¶
To get started, install @maplibre/ngx-maplibre-gl together with maplibre-gl.
npm install @maplibre/ngx-maplibre-gl maplibre-gl
Then import the MapLibre GL JS CSS in your global styles.
@import "maplibre-gl/dist/maplibre-gl.css";
Template ¶
With ngx-maplibre-gl, the map is rendered declaratively in the Angular template. The map, controls, markers, and popups are represented by Angular
components and directives, so there is no need to instantiate maplibregl.Map directly.
This example uses a checkbox to toggle markers for the five highest mountain peaks in Switzerland. The map itself is rendered with mgl-map, the
navigation and scale controls are added with mgl-control, and each mountain is rendered as an mgl-marker. The popup content is defined directly
in the template with mgl-popup.
<mgl-map
class="map-container"
[center]="center()"
[fitBounds]="showMarkers() ? (bounds() ?? undefined) : undefined"
[fitBoundsOptions]="fitBoundsOptions"
[mapStyle]="mapStyle"
[zoom]="zoom()"
>
<mgl-control mglNavigation />
<mgl-control mglScale [maxWidth]="100" unit="metric" />
@for (mountain of visibleMountains(); track mountain.name) {
<mgl-marker #mountainMarker [lngLat]="mountain.coordinates" anchor="center">
<div class="mountain-marker" [title]="mountain.name"></div>
</mgl-marker>
<mgl-popup
[marker]="mountainMarker"
[closeButton]="true"
[closeOnClick]="false"
[offset]="25"
>
<div class="popup-title">{{ mountain.name }}</div>
<div class="popup-info">
<strong>Elevation:</strong>
{{ mountain.elevation.toLocaleString() }} m<br />
<strong>Mountain Range:</strong> {{ mountain.range }}<br />
<strong>First Ascent:</strong> {{ mountain.firstAscent }}<br /><br />
{{ mountain.description }}
</div>
</mgl-popup>
}
</mgl-map>
Component logic ¶
The component class only provides data and reactive state. Signals are used to track whether markers are visible, compute the list of mountains to
render, and calculate the bounds passed to fitBounds. When the checkbox is cleared, the map returns to its initial center and zoom.
protected readonly mapStyle =
'https://vectortiles.geo.admin.ch/styles/ch.swisstopo.basemap.vt/style.json';
protected readonly initialCenter: [number, number] = [8.2312, 46.8182];
protected readonly initialZoom: [number] = [7];
protected readonly fitBoundsOptions = {
padding: 50,
maxZoom: 10,
duration: 1000,
};
protected readonly visibleMountains = computed(() =>
this.showMarkers() ? this.mountains : [],
);
protected readonly center = computed<[number, number]>(() =>
this.showMarkers() ? [7.7873, 46.007] : this.initialCenter,
);
protected readonly zoom = computed<[number]>(() =>
this.showMarkers() ? [8] : this.initialZoom,
);
protected readonly bounds = computed<LngLatBoundsLike | null>(() => {
if (!this.showMarkers()) {
return null;
}
const bounds = new LngLatBounds();
this.mountains.forEach((mountain) => {
bounds.extend(mountain.coordinates);
});
return bounds;
});
protected toggleMarkers(event: Event): void {
const target = event.target as HTMLInputElement;
this.showMarkers.set(target.checked);
}
Compared to the plain JavaScript version, the Angular example is more declarative. Instead of manually creating and removing Map, Marker, and Popup instances, Angular state drives what is rendered. The wrapper still exposes the underlying MapLibre GL JS features, but it fits more naturally into Angular's component model. You can still use the MapLibre GL JS API for more advanced use cases, but for common interactions like showing and hiding markers and popups, the template bindings are usually sufficient.
Wrapping Up ¶
In this blog post, I only covered a small subset of MapLibre GL JS's functionality. For a complete list of available methods, options, and events, check the API documentation. The examples section contains many more demos that show different features and use cases, and the guides page includes articles about performance optimization and migrating from other mapping libraries to MapLibre GL JS.
MapLibre GL JS is a flexible library for embedding interactive maps into web applications. It supports both raster and vector tiles, and it works with a wide variety of map styles and data sources. With its plugin ecosystem, you can easily extend its functionality to fit your specific use case.