DEV Community

Cover image for Step-by-Step: Custom Markers in MapLibre GL with Geoapify Marker API
Geoapify
Geoapify

Posted on

Step-by-Step: Custom Markers in MapLibre GL with Geoapify Marker API

MapLibre GL JS is an open-source JavaScript library for rendering vector maps with WebGL. It’s a great choice for modern, interactive maps—and a popular alternative to Mapbox GL JS.

Unlike Leaflet, MapLibre GL doesn’t come with built-in support for custom HTML marker icons. But with just a few lines of code, you can add dynamic, retina-ready markers using the Geoapify Map Marker API.

In this tutorial, you’ll learn two ways to add custom markers to a MapLibre GL map:

Both approaches use marker icons generated on-the-fly by the Geoapify Marker API, so you don’t need to design or host images manually.

Let’s start by setting up the map.


Table of Contents


Step 1: Set Up the Map with MapLibre GL and Geoapify Tiles

To begin, we’ll set up a basic MapLibre GL map and use free vector tiles from Geoapify.

1.1. Add HTML and Include MapLibre GL

Create a container for the map and include MapLibre GL from a CDN:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>MapLibre GL + Geoapify</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link href="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.css" rel="stylesheet" />
  <style>
    #map {
      height: 100vh;
      width: 100%;
    }
  </style>
</head>
<body>
  <div id="map"></div>
  <script src="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

1.2. Initialize the Map with Geoapify Tiles

Inside a <script> tag, initialize the map and set the tile source. You’ll need a Geoapify API key:

// Statue of Liberty coordinates
const pinCoords = [-74.0445481714432, 40.6892534];

const map = new maplibregl.Map({
  container: 'map',
  style: `https://maps.geoapify.com/v1/styles/osm-bright/style.json?apiKey=YOUR_GEOAPIFY_API_KEY`,
  center: pinCoords,
  zoom: 11
});
Enter fullscreen mode Exit fullscreen mode

🗝️ Don’t forget to replace YOUR_GEOAPIFY_API_KEY with your actual key. You can get one for free at Geoapify My Projects.

Once this is done, you’ll see a full-screen interactive map powered by Geoapify vector tiles.


Step 2: Add a Single Custom Marker Icon

Let’s add a single, styled marker using an HTML element and a dynamic icon from the Geoapify Marker API.

We'll place the marker at the Statue of Liberty and display a popup on click.

2.1. Define Marker Size and Create the Element

In this step, we prepare a DOM element to serve as our custom marker on the map.

const markerIconSize = {
  iconWidth: 45,
  iconHeight: 60,
  iconHeightWithShadow: 66,
};
Enter fullscreen mode Exit fullscreen mode

We define an object markerIconSize that helps manage the dimensions of the marker:

  • iconWidth: the width of the marker icon in pixels.
  • iconHeight: the height of the actual icon (without shadow).
  • iconHeightWithShadow: the total height including drop shadow (if any).
const el = document.createElement("div");
el.className = "marker";
Enter fullscreen mode Exit fullscreen mode

We create a <div> element that will act as the marker and assign it a class name (optional, for styling).

el.style.backgroundImage = `url(https://api.geoapify.com/v2/icon/?type=awesome&color=%23c3e2ff&size=60&icon=monument&contentSize=25&contentColor=%232b2b2b&noWhiteCircle&scaleFactor=2&apiKey=${myAPIKey})`;
Enter fullscreen mode Exit fullscreen mode

We apply a Geoapify-generated marker icon as the background image of the element. This URL defines:

  • type=awesome: use a Font Awesome marker shape
  • color=%23c3e2ff: light blue background
  • icon=monument: monument symbol
  • contentSize=25: inner icon size
  • contentColor=%232b2b2b: dark gray icon
  • noWhiteCircle: remove white halo
  • scaleFactor=2: make the icon retina-ready
el.style.backgroundSize = "contain";
el.style.backgroundRepeat = "no-repeat";
Enter fullscreen mode Exit fullscreen mode

These lines make sure the background image fits perfectly inside the element and doesn't repeat.

el.style.width = `${markerIconSize.iconWidth}px`;
el.style.height = `${markerIconSize.iconHeightWithShadow}px`;
Enter fullscreen mode Exit fullscreen mode

We then apply the previously defined dimensions to the marker element, giving it the correct size based on the icon and its shadow.

2.2. Add the Marker to the Map

Now that we've created and styled our marker element, we use MapLibre GL's Marker class to position it on the map.

new maplibregl.Marker({
  element: el,
  anchor: "bottom",
  offset: [0, markerIconSize.iconHeightWithShadow - markerIconSize.iconHeight],
})
Enter fullscreen mode Exit fullscreen mode

What this does:

  • element: el — Specifies the custom DOM element we created in Step 2.1.
  • anchor: "bottom" — Aligns the marker so that the bottom of the element (usually the tip of the pin) sits on the exact map coordinate.
  • offset: [...] — Corrects the vertical alignment by shifting the marker upward by the shadow height (in this case, 6 pixels), so that the visible icon—not the invisible shadow—touches the map point.
  .setLngLat([-74.0445481714432, 40.6892534])
Enter fullscreen mode Exit fullscreen mode

This sets the geographic coordinates where the marker should appear — in this example, it's placed on the Statue of Liberty.

  .setPopup(
    new maplibregl.Popup({
      offset: { bottom: [0, -markerIconSize.iconHeight] },
    }).setHTML("Statue of Liberty")
  )
Enter fullscreen mode Exit fullscreen mode

Here we define a popup that appears when the marker is clicked:

  • The offset positions the popup above the icon, not above the shadow.
  • setHTML(...) defines the content that will be shown inside the popup.
  .addTo(map);
Enter fullscreen mode Exit fullscreen mode

Finally, the .addTo(map) method adds the fully configured marker to the map.

Result

You now have a DOM-based custom marker aligned precisely on the map using a Geoapify-generated icon—with an optional popup.

👉 Try this in JSFiddle


Step 3: Add a Layer of Custom Markers from GeoJSON

When working with many markers—like cafés in a city—it’s more efficient to render them as a symbol layer. In this step, we’ll load places from the Geoapify Places API, register a custom marker icon from the Marker API, and display the results using a GeoJSON source.

3.1. Load Places from Geoapify Places API

We define a bounding box around Paris and request up to 100 cafés within that area:

const bounds = {
  lat1: 48.880021,
  lon1: 2.341083,
  lat2: 48.863956,
  lon2: 2.368348
};

const type = "catering.cafe";

const placesUrl = `https://api.geoapify.com/v2/places?categories=${type}&filter=rect:${bounds.lon1},${bounds.lat1},${bounds.lon2},${bounds.lat2}&limit=100&apiKey=${myAPIKey}`;

fetch(placesUrl)
  .then(response => response.json())
  .then(places => {
    showGeoJSONPoints(places, type);
  });
Enter fullscreen mode Exit fullscreen mode

This fetches places as a GeoJSON object and passes it to the showGeoJSONPoints() function.

3.2. Load and Register a Custom Marker Icon

We generate a pink coffee icon using the Geoapify Marker API, and register it under the name "rosa-pin":

const scale = 2;

map.loadImage(
  `https://api.geoapify.com/v2/icon/?icon=coffee&scaleFactor=${scale}&color=%23ff9999&size=45&type=awesome&apiKey=${myAPIKey}`,
  (error, image) => {
    if (error) throw error;

    map.addImage("rosa-pin", image, { pixelRatio: scale });
  }
);
Enter fullscreen mode Exit fullscreen mode

Using scaleFactor=2 and pixelRatio: 2 ensures high-resolution icons, especially on retina displays.

3.3. Add the GeoJSON Source and Symbol Layer

We now add the data and layer to the map, and configure interactivity:

function showGeoJSONPoints(geojson, id) {
  const layerId = `${id}-layer`;

  // Remove old source and layer if already present
  if (map.getSource(id)) {
    map.removeLayer(layerId);
    map.removeSource(id);
  }

  // Add GeoJSON source
  map.addSource(id, {
    type: "geojson",
    data: geojson
  });

  // Add symbol layer using the registered icon
  map.addLayer({
    id: layerId,
    type: "symbol",
    source: id,
    layout: {
      "icon-image": "rosa-pin",
      "icon-anchor": "bottom",
      "icon-offset": [0, 5],
      "icon-allow-overlap": true
    }
  });

  // Show popup on click
  map.on("click", layerId, function (e) {
    const coordinates = e.features[0].geometry.coordinates.slice();
    const name = e.features[0].properties.name;

    // Handle map wrapping for repeated features
    while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
      coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
    }

    new maplibregl.Popup({
      anchor: "bottom",
      offset: [0, -50]
    })
      .setLngLat(coordinates)
      .setText(name)
      .addTo(map);
  });

  // Change cursor to pointer on hover
  map.on("mouseenter", layerId, function () {
    map.getCanvas().style.cursor = "pointer";
  });

  map.on("mouseleave", layerId, function () {
    map.getCanvas().style.cursor = "";
  });
}
Enter fullscreen mode Exit fullscreen mode

This code:

  • Removes any previous layer with the same ID
  • Adds the new data source
  • Creates a symbol layer that uses the "rosa-pin" icon
  • Adds a popup on click showing the location name
  • Changes the cursor on hover for better UX

Result

You now have a clean, efficient way to display multiple points of interest with custom Geoapify icons, interactive popups, and excellent visual performance.

👉 Try it live on JSFiddle


Step 4: Summary and Live Demos

In this tutorial, you learned how to enhance your MapLibre GL maps using custom marker icons generated by the Geoapify Map Marker API.

We covered two practical approaches:

✅ Single Custom Marker with Popup

  • Uses an HTML element with a Geoapify icon as background
  • Ideal for a small number of manually positioned markers

👉 Live demo: Single marker (Statue of Liberty)

✅ Marker Layer from GeoJSON and Places API

  • Loads real location data via the Geoapify Places API
  • Registers one custom icon and renders all points as a symbol layer
  • Supports popups and hover interactions

👉 Live demo: Marker layer (Cafés in Paris)

Why use Geoapify with MapLibre GL?

  • Retina-ready markers: generated on the fly, no image hosting needed
  • Flexible styling: icon type, size, color, and shape controlled via URL
  • Fully open-source: MapLibre GL is a great alternative to proprietary platforms
  • Scalable rendering: symbol layers are optimized for many markers

Useful Links

Have questions or ideas for extending this example (e.g., clustering or filters)? Drop a comment or fork the JSFiddle!

Top comments (0)