Home | Send Feedback | Share on Bluesky |

Embedding maps with MapTiler in a web application

Published: 21. July 2025  •  javascript

In a previous blog post, I showed you how to write a web application that displays photos on a map. The article demonstrated how to extract GPS coordinates and then display a marker for each photo on a Google Map.

In this blog post, I want to show an alternative to Google Maps: MapTiler. MapTiler is a company that provides services around maps based on OpenStreetMap data. They offer customizable street and satellite maps, developer APIs, map hosting, and geocoding, making it a versatile alternative to other mapping platforms.

To get started with MapTiler, you need to create an account on their website: https://www.maptiler.com/
You can create a free account that can be used for non-commercial projects.

Once you have created an account and logged into the platform, create a new API key. Enter a name and description, and most importantly, enter a domain in the "Allowed HTTP Origins" field. For testing locally, enter "localhost". The Allowed HTTP Origins setting is important because the API key is visible in the client-side JavaScript code, and if you don't specify the allowed origins, anyone can use your API key. When you add a domain, only requests from that domain will be allowed to use the API key.

Setup

MapTiler provides a JavaScript SDK that you can use to display maps in your web application. To get started with the SDK, first install it:

npm install @maptiler/sdk

Add a div element to your HTML where the map will be displayed:

  <div id="map"></div>

index.html

Add a CSS rule for this div element to set its size. The div must have non-zero height.

  <style>
    body { margin: 0; padding: 0; }
    #map { position: absolute; top: 0; bottom: 0; width: 100%; }
  </style>

index.html

Next, add the CSS rules for the map itself. MapTiler provides a CSS file that you can import. If you use a bundler that supports CSS imports, you can add the following line to your JavaScript file. I'm using Vite in this example, which does support CSS imports:

import '@maptiler/sdk/dist/maptiler-sdk.css';

main.js

Alternatively, you can also link the CSS file in your HTML file from the MapTiler CDN. Make sure that the version specified here matches the version of the SDK in your package.json file.

<link href='https://cdn.maptiler.com/maptiler-sdk-js/v3.6.1/maptiler-sdk.css' rel='stylesheet' />

Next, import the MapTiler SDK in your JavaScript file.

import * as maptilersdk from '@maptiler/sdk';

main.js

Set the API key.

maptilersdk.config.apiKey = 'W61XbXMJwzZapVydUu4s';

main.js

And finally, create the map. The map can be configured in various ways. The container option specifies the ID of the div element where the map will be displayed. Then you can choose between a globe and mercator projection. You can specify the initial center and zoom level. The style option allows you to choose a predefined style for the map. MapTiler provides several styles, such as BASIC, SATELLITE, and STREETS. The project page of the MapTiler SDK has a list of available styles with screenshots.

const map = new maptilersdk.Map({
  container: 'map',
  projection: 'globe',
  style: maptilersdk.MapStyle.HYBRID,
  center: [8.5417, 47.3769],
  zoom: 12
});

main.js

To see all available options for the map, check the documentation page.

The map instance can raise various events. For this application, the load event is used. The map fires this event after all necessary resources have been downloaded and the first visually complete rendering of the map has occurred.

map.on('load', () => {
  loadPhotos();
});

main.js

This is a good time to load all data about the photos and display markers on the map.

Markers

The MapTiler SDK provides the Marker class to display markers on the map.

const marker = new maptilersdk.Marker()
  .setLngLat([30.5, 50.5])
  .addTo(map);

This is the easiest way to add markers to the map if you only have a few markers to display. But if you start adding many markers, especially when they are located close to each other, it becomes difficult for the user to interact with them. In this application, we want to show the pictures when the user clicks on a marker.

A better way to display many points on a map is to use a cluster. That means that the markers are grouped together in clusters, which are displayed as a single marker. When the user clicks on a cluster, it expands and shows the individual markers inside the cluster.

The SDK provides everything we need out of the box. First, we add all the locations of the photos to the map with the addSource method. For this, we need the coordinates of the photos in a GeoJSON format. The metadata for the photos is stored in a JSON that does not follow the GeoJSON format. Therefore, the application first converts this JSON into a GeoJSON format.

  let bounds = new maptilersdk.LngLatBounds();

  const geojson = {
    type: 'FeatureCollection',
    features: photos.map(photo => {
      bounds.extend([photo.lng, photo.lat]);
      return {
        type: 'Feature',
        geometry: {
          type: 'Point',
          coordinates: [photo.lng, photo.lat]
        },
        properties: {
          img: photo.img,
          ts: photo.ts
        }
      };
    })
  };

main.js

During the conversion, the application keeps track of the bounding box of all photos in the bounds variable. This is used later to pan and zoom the map so that all markers/clusters are visible.

  map.fitBounds(bounds, {
    padding: 20
  });

main.js


Next, the code adds the GeoJSON to the map with addSource. This method is versatile and allows you to add various data sources to the map, for example, raster tiles, vector tiles, images, videos, canvas, and GeoJSON data.

  map.addSource('photos', {
    type: 'geojson',
    data: geojson,
    cluster: true,
    clusterMaxZoom: 14,
    clusterRadius: 50
  });

main.js

This by itself does not show anything on the map. The application needs to add layers to the map that define how the data should be displayed.

The application adds three layers to the map.

First layer is the cluster layer, which is displayed as a circle. The color and radius of the circle depend on the number of points in the cluster. The step expression is used to define different colors and sizes for different ranges of point counts.

  map.addLayer({
    id: 'clusters',
    type: 'circle',
    source: 'photos',
    filter: ['has', 'point_count'],
    paint: {
      'circle-color': [
        'step',
        ['get', 'point_count'],
        '#51bbd6',
        100,
        '#f1f075',
        750,
        '#f28cb1'
      ],
      'circle-radius': [
        'step',
        ['get', 'point_count'],
        20,
        100,
        30,
        750,
        40
      ]
    }
  });

main.js

Next layer is the cluster count layer, which displays the number of points in the cluster as text.

  map.addLayer({
    id: 'cluster-count',
    type: 'symbol',
    source: 'photos',
    filter: ['has', 'point_count'],
    layout: {
      'text-field': '{point_count_abbreviated}',
      'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
      'text-size': 12
    }
  });

main.js

The third layer shows the individual markers, which are displayed as circles. The filter ['!', ['has', 'point_count']] ensures that only unclustered points are displayed.

  map.addLayer({
    id: 'unclustered-point',
    type: 'circle',
    source: 'photos',
    filter: ['!', ['has', 'point_count']],
    paint: {
      'circle-color': '#11b4da',
      'circle-radius': 8,
      'circle-stroke-width': 1,
      'circle-stroke-color': '#fff'
    }
  });

main.js

To learn more about layers, check out the reference documentation

Events

As the last step, the application configures two event listeners.

The first event listener is a click event listener for the cluster layer. When the user clicks on a cluster circle, the map should zoom in on the map and expand the cluster. For this purpose, the source object provides the method getClusterExpansionZoom that returns the zoom at which the given cluster expands. With the easeTo method, the map is animated to the new zoom level.

  map.on('click', 'clusters', async function (e) {
    const features = map.queryRenderedFeatures(e.point, {
      layers: ['clusters']
    });
    const clusterId = features[0].properties.cluster_id;
    const zoom = await map.getSource('photos').getClusterExpansionZoom(clusterId);
    map.easeTo({
      center: features[0].geometry.coordinates,
      zoom
    });
  });

main.js

The second event listener is a click event listener for the unclustered points. When the user clicks on an individual marker, a popup is displayed with the image thumbnail, the timestamp, and the coordinates of the photo. The popup also contains a button to open the full image in a lightbox gallery.

To show popups, the SDK provides the Popup component. You only need to specify the coordinates where the popup should be displayed and the HTML content of the popup.

  map.on('click', 'unclustered-point', function (e) {
    const coordinates = e.features[0].geometry.coordinates.slice();
    const img = e.features[0].properties.img;

    while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
      coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
    }

    const ts = e.features[0].properties.ts;
    const lat = e.features[0].geometry.coordinates[1];
    const lng = e.features[0].geometry.coordinates[0];
    const timestamp = ts ? format(new Date(ts * 1000), 'yyyy-MM-dd HH:mm') : '';

    new maptilersdk.Popup()
      .setLngLat(coordinates)
      .setHTML(`<img src="assets/thumbnails/${img}" alt="${img}" style="width: 100px;"/><br/><div>${img}</div><div>${timestamp}</div><div>Lat: ${lat}<br>Lng: ${lng}</div><button class="open-lightgallery">Open Lightgallery</button>`)
      .addTo(map);
  });
}

main.js


This video shows you the result of all this code.
MapTiler Example

Conclusion

MapTiler is a powerful alternative to other mapping solutions, such as Google Maps, that provides a wide range of features and customization options. The SDK makes it easy to display maps, markers, and clusters in your web application. The free tier for non-commercial usage makes it easy to get started and experiment with the platform.

Check out MapTiler's homepage to get an overview of all the services they offer.