Mapbox GL JS Tutorial: Build a Custom Interactive Map with Markers, Popups, and GeoJSON

Why Mapbox GL JS? From Static Maps to Dynamic Experiences

When you need a map that does more than show static points, Mapbox GL JS is the tool to reach for. It builds interactive web maps using WebGL, rendering vector tiles directly in the browser. This means smooth zooming, dynamic styling, and the ability to visualize complex datasets with performance that traditional raster-based libraries struggle to match. While tools like Leaflet excel at simplicity, Mapbox GL JS opens the door to design-heavy projects, 3D terrain, and rich, data-driven visualizations. We’ll cover its trade-offs, including cost considerations, so you can make an informed choice for your project.

Mapbox GL JS vs. Leaflet: Choosing the Right Tool

Your project’s needs determine the best library.

  • Use Leaflet for straightforward marker-based maps, quick prototypes, or when open-source simplicity is paramount. It’s lightweight, has a vast plugin ecosystem, and is easier to grasp for basic tasks.
  • Choose Mapbox GL JS for design-heavy projects, complex data layers (GeoJSON, vector tiles), 3D terrain, smooth animations, and when you want to leverage Mapbox’s integrated ecosystem (Studio, Directions, Geocoding). It demands more upfront setup but offers finer visual control.

What You’ll Build in This Tutorial

By the end of this guide, you’ll have a fully interactive map centered on a custom location. It will feature multiple styled markers with clickable popups showing custom HTML content, and a choropleth layer built from an external GeoJSON file, colored based on data properties. You’ll gain practical skills in: initializing a Mapbox map with a custom style, securely managing access tokens, adding markers and popups, and loading, styling, and adding interactivity to GeoJSON data sources.

Project Setup: Your First Mapbox GL JS Map

Let’s get a map on the page. Start with this minimal HTML boilerplate. Ensure you have a div with an id for the map container and include the Mapbox GL JS and CSS files via CDN.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My Mapbox Map</title>
    <script src='https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.js'></script>
    <link href='https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.css' rel='stylesheet' />
    <style>
        body { margin: 0; padding: 0; }
        #map { position: absolute; top: 0; bottom: 0; width: 100%; }
    </style>
</head>
<body>
    <div id="map"></div>
    <script>
        mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN'; // Replace this!
        const map = new mapboxgl.Map({
            container: 'map', // container ID
            style: 'mapbox://styles/mapbox/streets-v12', // style URL
            center: [-74.5, 40], // starting position [lng, lat]
            zoom: 9 // starting zoom
        });
    </script>
</body>
</html>

The two critical ingredients are your accessToken and a style URL. The token authenticates your requests, and the style defines the visual appearance of every map layer.

Securing Your Mapbox Access Token (Critical Step!)

Never hardcode your access token in frontend code committed to a public GitHub repository. Exposed tokens can be stolen, leading to unauthorized usage and unexpected charges.

For production applications, use environment variables. In a build process (like with Webpack or Vite), you can inject the token from a .env file that is added to .gitignore.

// In your JavaScript/React build process
mapboxgl.accessToken = process.env.MAPBOX_ACCESS_TOKEN;

For purely static sites without a build step, your last line of defense is to use Mapbox’s token scoping features in your Mapbox account dashboard. Restrict the token to specific URLs to limit damage if it is exposed.

Customizing the Base Map: It’s All About Style

The style URL is your gateway to a unique map design. Log into Mapbox Studio, create a new style, or modify an existing template. You can change road colors, hide points of interest, adjust label fonts, and more. Once published, Studio provides you with a new style URL. Update the style option in your map initialization code:

style: 'mapbox://styles/your-username/your-style-id' // Your custom style

This instantly transforms your map’s aesthetic to match your project’s brand.

Adding Interactivity: Markers and Popups

Markers and popups are the fundamental building blocks of user interaction. Mapbox provides clean APIs for both.

// Create a marker at a specific coordinate
const marker = new mapboxgl.Marker()
    .setLngLat([-74.5, 40])
    .addTo(map);

// Create a popup and attach it to the marker
const popup = new mapboxgl.Popup({ offset: 25 }) // offset popup from marker
    .setHTML('<h3>Hello World!</h3><p>This is a popup.</p>');

marker.setPopup(popup);
// The popup will open when the marker is clicked

Popups can contain formatted HTML, including images, links, and buttons, allowing you to create rich information cards.

Creating Custom Marker HTML Elements

Move beyond the default pin by passing a custom HTMLElement to the Marker constructor. This lets you match your site’s design system.

// Create a custom element
const el = document.createElement('div');
el.className = 'custom-marker';
el.style.backgroundColor = '#3bb2d0';
el.style.border = '3px solid #fff';
el.style.borderRadius = '50%';
el.style.width = '30px';
el.style.height = '30px';
el.style.boxShadow = '0 2px 6px rgba(0,0,0,0.3)';

// Use the element for the marker
const customMarker = new mapboxgl.Marker(el)
    .setLngLat([-74.5, 40.1])
    .addTo(map);

Managing Multiple Markers and Events

You’ll typically create markers dynamically from data. Loop through an array and attach event listeners.

const locations = [
    { lnglat: [-74.5, 40], name: 'Location A' },
    { lnglat: [-74.6, 40.1], name: 'Location B' },
];

locations.forEach(loc => {
    const marker = new mapboxgl.Marker()
        .setLngLat(loc.lnglat)
        .setPopup(new mapboxgl.Popup().setText(loc.name))
        .addTo(map);

    marker.getElement().addEventListener('mouseenter', () => {
        marker.togglePopup(); // Open popup on hover
    });
});

For maps with hundreds of markers, this simple approach can impact performance. We’ll discuss solutions like clustering later.

Leveling Up: Visualizing Data with GeoJSON

GeoJSON is the standard format for representing geographic features (points, lines, polygons) in JavaScript. Mapbox GL JS can load and style GeoJSON data dynamically, unlocking powerful visualizations like choropleth maps.

First, add a GeoJSON source to your map. You can load data from a URL or a local JavaScript object.

map.on('load', () => {
    // Add a GeoJSON source
    map.addSource('my-data', {
        type: 'geojson',
        data: 'https://raw.githubusercontent.com/your-repo/data.geojson' // Your GeoJSON URL
    });

    // Add a layer to visualize the source
    map.addLayer({
        id: 'data-layer',
        type: 'fill', // Could be 'line', 'circle', 'symbol'
        source: 'my-data',
        paint: {
            'fill-color': '#3bb2d0',
            'fill-opacity': 0.5
        }
    });
});

Always add sources and layers inside the map’s load event to ensure the map is ready.

Styling GeoJSON Layers: Creating Choropleth Maps

Data-driven styling is where Mapbox shines. Use expressions to color features based on their properties.

map.addLayer({
    id: 'choropleth',
    type: 'fill',
    source: 'my-data',
    paint: {
        'fill-color': [
            'interpolate',
            ['linear'],
            ['get', 'populationDensity'], // Property from your GeoJSON
            0, '#f7fbff',
            100, '#9ecae1',
            500, '#3182bd'
        ],
        'fill-opacity': 0.7
    }
});

The interpolate expression creates a smooth color gradient. For discrete values, use match.

Adding Interactivity to GeoJSON Layers

Make your data layer clickable to reveal feature properties.

map.on('click', 'choropleth', (e) => {
    const properties = e.features[0].properties;
    new mapboxgl.Popup()
        .setLngLat(e.lngLat)
        .setHTML(`
            <h4>${properties.regionName}</h4>
            <p>Density: ${properties.populationDensity} people/sq km</p>
        `)
        .addTo(map);
});

// Change cursor on hover
map.on('mouseenter', 'choropleth', () => {
    map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'choropleth', () => {
    map.getCanvas().style.cursor = '';
});

Performance & Production Considerations

As your map grows, performance becomes critical. Rendering thousands of raw GeoJSON features or markers can slow down the browser.

For points, implement marker clustering using a plugin like mapbox-gl-cluster to group nearby points. For large polygon datasets, consider converting your GeoJSON into vector tiles using a tool like Tippecanoe and serving them via Mapbox Studio or your own tile server. This drastically improves rendering speed.

Also, implement viewport-aware lazy loading: only request data for the currently visible map area instead of loading a massive global dataset upfront.

Understanding Mapbox Pricing and Avoiding Bill Shock

Mapbox uses a pay-as-you-go model based primarily on Monthly Active Users (MAUs). A MAU is a unique user who loads a map that uses your token in a given month. The free tier includes 50,000 MAUs, 50,000 style loads, and 125,000 geocodes per month.

Costs scale with usage. A high-traffic public dashboard could consume tens of thousands of MAUs quickly. Monitor your usage in the Mapbox dashboard. If your project is budget-sensitive or expects massive scale, the cost can become a significant factor.

The Vendor Lock-In Question: Mapbox vs. MapLibre

Using Mapbox GL JS with Mapbox styles and tiles creates a form of vendor lock-in. Your map’s appearance and tile data are tied to Mapbox’s platform.

MapLibre GL JS is an open-source fork of the Mapbox GL JS library. It uses the same API, allowing you to swap the library with minimal code changes, but you must provide your own map styles and tile servers (e.g., using OpenStreetMap data). This gives you full control and eliminates per-MAU fees, but introduces infrastructure complexity.

Your decision framework:

  • Choose Mapbox for speed to market, design ease with Studio, and a robust, managed ecosystem. Accept the ongoing cost.
  • Evaluate MapLibre if you require full control, have extreme scaling needs, operate under strict data sovereignty requirements, or have a tight, long-term budget.

Next Steps and Going Beyond the Basics

You now have a functional, interactive map. The path forward includes integrating with modern frameworks, optimizing performance, and exploring advanced features.

Explore adding 3D terrain with map.addLayer for raster-dem sources. Create animated flight paths between points. Use the turf.js library alongside Mapbox for client-side spatial analysis (buffers, unions, calculations). Implement custom camera animations for guided tours.

Integrating Your Map into a React or Next.js App

For React applications, use the official react-map-gl library. It provides React-friendly components and handles cleanup.

import React, { useRef, useEffect } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';

function MapboxMap() {
    const mapContainer = useRef(null);
    const map = useRef(null);

    useEffect(() => {
        if (map.current) return; // Initialize map only once
        mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN;
        map.current = new mapboxgl.Map({
            container: mapContainer.current,
            style: 'mapbox://styles/mapbox/streets-v12',
            center: [-74.5, 40],
            zoom: 9
        });

        // Cleanup on component unmount
        return () => map.current.remove();
    }, []);

    return <div ref={mapContainer} style={{ width: '100%', height: '500px' }} />;
}

In Next.js, ensure the component is client-side only using dynamic import with ssr: false or the 'use client' directive to avoid server-side rendering issues with WebGL.

Troubleshooting Common Mapbox GL JS Issues

  • Map not loading: Check your access token and network connection. Ensure the token is not expired or restricted. Check browser console for CORS errors.
  • Markers not showing: Verify your coordinates are in [longitude, latitude] order. For custom markers, check that your CSS isn’t hiding the element (e.g., display: none).
  • Layers missing: Confirm the source ID in map.addLayer matches the ID used in map.addSource. Check the layer’s minzoom and maxzoom properties—the layer might only be visible at certain zoom levels.
  • Rendering glitches or memory leaks: In Single Page Applications, ensure you call map.remove() when the component unmounts. Avoid adding sources/layers on every render without first checking if they exist.

You’ve moved beyond a simple basemap. You can now create styled, interactive, data-rich mapping applications with Mapbox GL JS. Remember to weigh its power against its cost and consider the open-source MapLibre path for projects where control and scaling are paramount.

Scroll to Top