Skip to content

Vector maps for the web with MapLibre GL JS

Free

Starter

Standard

Professional

MapLibre GL JS is a robust, fully free and open-source library with 3D rendering, dynamic styling, and many other features. Stadia Maps is a founding member of the MapLibre project, which facilitates the ongoing development of MapLibre GL JS and other rendering technologies.

In this tutorial, you'll learn how to add a map to your webpage and fill it with markers representing your store locations, delivery trucks, or whatever else you're mapping.

Drawing a map

Let's dive in with some example code on how to use our vector tiles in MapLibre GL JS.

For local development on a web server at localhost or 127.0.0.1, you can get started without any API keys or domain setup!

For mobile, backend, and non-local web development, you'll need either domain auth or an API key. If you don't already have a Stadia Maps account, sign up for a free to get started.

Domain-based authentication

Domain-based authentication is the easiest form of authentication for production web apps. No additional application code is required, and you don't need to worry about anyone scraping your API keys. We recommend this for most browser-based applications.

  1. Sign in to the client dashboard.
  2. Click "Manage Properties."
  3. Under "Authentication Configuration," click the button to add your domain.
API key authentication

Authenticating requests via API key

You can authenticate your requests by adding an API key to the query string or HTTP header.

The simplest is to add a query string parameter api_key=YOUR-API-KEY to your request URL. For example, to access the /tz/lookup API endpoint, your request URL might look like this.

Example URL with an API key placeholder
https://api.stadiamaps.com/tz/lookup/v1?lat=59.43696&lng=24.75357&api_key=YOUR-API-KEY

You can also use an Authorization HTTP header instead of a query string as shown below. Don't forget the Stadia-Auth prefix!

Example Authorization header with an API key placeholder
Authorization: Stadia-Auth YOUR-API-KEY

How to get an API key

Don't have an API key yet? Follow these easy steps!

  1. Sign in to the client dashboard. (If you don't have an account yet, sign up for free; no credit card required!)
  2. Click "Manage Properties."
  3. If you have more than one property (ex: for several websites or apps), make sure you have selected the correct property from the dropdown at the top of the page.
  4. Under "Authentication Configuration," you can generate, view or revoke your API key.

Video: How to generate your API key

To make experimenting even easier, we've packaged the example code as a JSFiddle playground. Click the "Try it in JSFiddle" button to start experimenting right from your web browser.

Try it in JSFiddle
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Vector Map Demo</title>
        <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
        <script type="text/javascript" src="//unpkg.com/maplibre-gl@4.0.2/dist/maplibre-gl.js"></script>
        <link href="//unpkg.com/maplibre-gl@4.0.2/dist/maplibre-gl.css" rel="stylesheet" />
        <style type="text/css">
            body {
              margin: 0;
              padding: 0;
            }

            #map {
              position: absolute;
              top: 0;
              bottom: 0;
              width: 100%;
            }
        </style>
    </head>
    <body>
        <div id="map"></div>
        <script type="text/javascript">
         var map = new maplibregl.Map({
           container: 'map',
           style: 'https://tiles.stadiamaps.com/styles/alidade_smooth.json',  // Style URL; see our documentation for more options
           center: [12, 53],  // Initial focus coordinate
           zoom: 4
         });

         // MapLibre GL JS does not handle RTL text by default,
         // so we recommend adding this dependency to fully support RTL rendering if your style includes RTL text
         maplibregl.setRTLTextPlugin('https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.min.js');

         // Add zoom and rotation controls to the map.
         map.addControl(new maplibregl.NavigationControl());
        </script>
    </body>
</html>

Code Walkthrough

First, we'll add some opening HTML content that will make the document standard compliant:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
        <title>Vector Map Demo</title>

Next, we'll include the MapLibre library files:

        <script type="text/javascript" src="//unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
        <link href="//unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" />

Now, we'll add CSS to handle the size of the body, the map ID, and then close the <head> tag:

        <style type="text/css">
            body {
              margin: 0;
              padding: 0;
            }

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

With the <head> details in, we can now add the body for our page. This is where we'll create the map element:

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

Next, we'll add the JavaScript for:

  • initializing the map, specifying the container, style, center, and zoom:
        <script type="text/javascript">
         var map = new maplibregl.Map({
           container: 'map',
           style: 'https://tiles.stadiamaps.com/styles/alidade_smooth.json',  // Style URL; see our documentation for more options
           center: [12, 53],  // Initial focus coordinate
           zoom: 4
         });
  • adding RTL support:
         // MapLibre GL JS does not handle RTL text by default,
         // so we recommend adding this dependency to fully support RTL rendering if your style includes RTL text
         maplibregl.setRTLTextPlugin('https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.min.js');
  • and adding navigation controls:
         // Add zoom and rotation controls to the map.
         map.addControl(new maplibregl.NavigationControl());
        </script>

Finally, we'll close up the <body> and <html> tags:

    </body>
</html>

And that's it. Congratulations.

All the code, put together, will match the same as the code at the start of this section.

Adding marker pins to the map

Once you have a map, you probably want to add some markers to it! The example below can serve as a starting point, and covers all the main components. You can easily substitute your own branded markers, style the popup (or remove it), etc. Please refer to the Marker documentation for additional details.

Try it in JSFiddle
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Vector Map Demo</title>
        <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
        <script type="text/javascript" src="//unpkg.com/maplibre-gl@4.0.2/dist/maplibre-gl.js"></script>
        <link href="//unpkg.com/maplibre-gl@4.0.2/dist/maplibre-gl.css" rel="stylesheet" />
        <style type="text/css">
        body {
            margin: 0;
            padding: 0;
        }

        #map {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 100%;
        }

        /* We recommend using an icon that is 2x the intended display size, so that it renders nicely on retina displays. */
        .marker {
            /* The following icon is owned by Stadia Maps. While you maintain an account with us, we grant you royalty-free use of
             * this image when displayed on our maps.
             */
            background-image: url('data:image/png;base64,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');
            background-size: cover;
            width: 27px;
            height: 42px;
            cursor: pointer;
        }
        </style>
    </head>
    <body>
        <div id="map"></div>
        <script type="text/javascript">
        var map = new maplibregl.Map({
            container: 'map',
            style: 'https://tiles.stadiamaps.com/styles/alidade_smooth_dark.json',  // Style URL; see our documentation for more options
            center: [6.942373, 50.332852],  // Initial focus coordinate (long, lat)
            zoom: 14
        });

        // Add zoom and rotation controls to the map.
        map.addControl(new maplibregl.NavigationControl());

        // First, we define our marker locations. You can use whatever format you want when
        // working with custom markers, but we have chosen to use GeoJSON for this example, as
        // a lot of geospatial data comes in this form. If you have a lot of data, you may want to
        // put it in another file that is loaded separately.
        var markerCollection = {
            "type": "FeatureCollection",
            "features": [{
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    // NOTE: in GeoJSON notation, LONGITUDE comes first. GeoJSON 
                    // uses x, y coordinate notation
                    "coordinates": [6.940046, 50.333947]
                },
                "properties": {
                    "title": "Nürburgring"
                }
            }]
        };

        // Next, we can add markers to the map
        markerCollection.features.forEach(function(point) {
            // Since these are HTML markers, we create a DOM element first, which we will later
            // pass to the Marker constructor.
            var elem = document.createElement('div');
            elem.className = 'marker';

            // Now, we construct a marker and set it's coordinates from the GeoJSON. Note the coordinate order.
            var marker = new maplibregl.Marker(elem);
            marker.setLngLat(point.geometry.coordinates);

            // You can also create a popup that gets shown when you click on a marker. You can style this using
            // CSS as well if you so desire. A minimal example is shown. The offset will depend on the height of your image.
            var popup = new maplibregl.Popup({ offset: 24, closeButton: false });
            popup.setHTML('<div>' + point.properties.title + '</div>');

            // Set the marker's popup.
            marker.setPopup(popup);

            // Finally, we add the marker to the map.
            marker.addTo(map);
        });
        </script>
    </body>
</html>

Code Walkthrough

To set up the page, we'll need the same initial code we used to draw the map:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Vector Map Demo</title>
        <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
        <script type="text/javascript" src="//unpkg.com/maplibre-gl@4.0.2/dist/maplibre-gl.js"></script>
        <link href="//unpkg.com/maplibre-gl@4.0.2/dist/maplibre-gl.css" rel="stylesheet" />
        <style type="text/css">
        body {
            margin: 0;
            padding: 0;
        }

        #map {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 100%;
        }

Next, we'll add the marker class, along with a base64 blob of the image we want to display for the marker:

        /* We recommend using an icon that is 2x the intended display size, so that it renders nicely on retina displays. */
        .marker {
            /* The following icon is owned by Stadia Maps. While you maintain an account with us, we grant you royalty-free use of
             * this image when displayed on our maps.
             */
            background-image: url('data:image/png;base64,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');
            background-size: cover;
            width: 27px;
            height: 42px;
            cursor: pointer;
        }

Then we'll close up the <style> and <head> tag:

        </style>
    </head>

Next, we'll add the <body> tag and map element:

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

And then we'll add the JavaScript to create the map, style and center it, along with setting the zoom level and controls:

        <script type="text/javascript">
        var map = new maplibregl.Map({
            container: 'map',
            style: 'https://tiles.stadiamaps.com/styles/alidade_smooth.json',  // Style URL; see our documentation for more options
            center: [6.942373, 50.332852],  // Initial focus coordinate (long, lat)
            zoom: 14
        });
        // Add zoom and rotation controls to the map.
        map.addControl(new maplibregl.NavigationControl());

With the map element created, we can now create our markerCollection object, which will store the coordinates for our markers as GeoJSON in this example:

        // You can use whatever format you want when working with custom markers, but we have chosen
        // to use GeoJSON for this example, as a lot of geospatial data comes in this form. If you
        // have a lot of data, you may want to put it in another file that is loaded separately.
        var markerCollection = {
            "type": "FeatureCollection",
            "features": [{
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    // NOTE: in GeoJSON notation, LONGITUDE comes first. GeoJSON 
                    // uses x, y coordinate notation
                    "coordinates": [6.940046, 50.333947]
                },
                "properties": {
                    "title": "Nürburgring"
                }
            }]
        };

Next, we'll write a function to handle the market components:

  • iterating through each feature in the markerCollection:
        // Next, we can add markers to the map
        markerCollection.features.forEach(function(point) {
            // Since these are HTML markers, we create a DOM element first, which we will later
            // pass to the Marker constructor.
            var elem = document.createElement('div');
            elem.className = 'marker';
  • constructing a marker and setting its coordinates from the GeoJSON object we created earlier:
            var marker = new maplibregl.Marker(elem);
            marker.setLngLat(point.geometry.coordinates);
  • adding a popup that will display the title we defined earlier:

Note

The offset value will depend on the height of your image.

            var popup = new maplibregl.Popup({ offset: 24, closeButton: false });
            popup.setHTML('<div>' + point.properties.title + '</div>');

            // Set the marker's popup.
            marker.setPopup(popup);
  • and then adding the marker to the map:
            // Finally, we add the marker to the map.
            marker.addTo(map);
        });

Finally, we'll close up the <script>, <body>, and <html> tags:

        </script>
    </body>
</html>

And that's it. Congratulations.

All the code, put together, will match the code at the start of this section.

Next Steps

This tutorial doesn't even begin to show what's possible with MapLibre GL JS. Now that you have a map and some markers, you can get inspiration from what others have built at the MapLibre GL JS Examples page. They have in-depth examples covering everything from GeoJSON lines to interactive time sliders. And don't forget to check out the plugins and the rest of the MapLibre GL JS documentation either.

Once you're ready to move beyond localhost testing, sign up for a free Stadia Maps account, and we'll walk through the next steps.

Get Started With a Free Account