DEV Community

Cover image for MapLibre GL Markers: Custom Icons, Popups & Events with Geoapify

MapLibre GL Markers: Custom Icons, Popups & Events with Geoapify

Markers are one of the most common elements in any interactive map. They help users identify important points, highlight search results, or drop a temporary pin on click.

In MapLibre GL, you can display points on the map in two main ways:

  1. Markers with new maplibregl.Marker() — these are DOM elements (HTML nodes) placed on top of the map at specific coordinates. They are highly customizable, can be interactive, and support drag & drop.
  2. Symbol layers — vector-based icons defined inside a GeoJSON source. They are more efficient for rendering thousands of points but offer less flexibility for custom HTML content.

In this article, we’ll focus on the Marker() API. Markers are ideal when you need interactive or custom pins — for example:

  • Dropping a pin at a clicked location.
  • Highlighting search results or selected places.
  • Adding temporary overlays during editing or debugging.

If you want to style your markers, you can use the Geoapify Map Marker Icon API, which lets you generate dynamically PNG icons with custom colors, sizes, and symbols. These can be easily plugged into MapLibre markers.

To see everything in action, check out the interactive demo: MapLibre Markers with Geoapify on CodePen.


Table of Contents

  1. Creating markers
  2. Custom markers
  3. Adding a popup to a marker
  4. Marker events
  5. Managing markers
  6. When not to use markers
  7. FAQ

1. Creating markers

The easiest way to add a marker in MapLibre GL is by creating a new instance of maplibregl.Marker.

The constructor can take an optional configuration object — MarkerOptions — that controls the marker’s appearance (custom HTML, anchor, offset) and behavior (draggable, rotation, alignment).

Once created, you place the marker on the map by setting its position with .setLngLat([lon, lat]) and finalizing it with .addTo(map):

new maplibregl.Marker(markerOptions)
  .setLngLat([lon, lat])
  .addTo(map);
Enter fullscreen mode Exit fullscreen mode

This creates a default blue pin marker at the given coordinates:

MapLibreGL: blue default marker

MarkerOptions: customizing behavior and appearance

When creating a marker, you can pass an optional MarkerOptions object to control how it looks and behaves. This allows you to change its anchor position, make it draggable, or even replace it entirely with a custom HTML element.

Here are some of the most commonly used options:

Option Description Example
anchor Defines which part of the marker should be placed on the given coordinates. Options: center, top, bottom, left, right, top-left, top-right, bottom-left, bottom-right. Default is center. anchor: "bottom"
className Space-separated CSS class names to add to the marker element. className: "my-marker highlight"
draggable Makes the marker draggable so users can move it on the map. Default is false. draggable: true
element A custom HTML element to use instead of the default blue pin. element: myDivElement
offset Pixel offset from the anchor point. Useful to align custom markers with the correct "tip". offset: [0, -10]

The offset option becomes especially important when you’re using custom marker icons.

By default, a marker is placed with its center aligned to the given coordinates. However, many icons — like pin-shaped markers — need to be aligned at the bottom tip rather than the center.

We’ll explore this in more detail later in the Custom markers section.

📖 There are more options available, such as color, scale, rotation, opacity, pitchAlignment, and others.

See the full list in the official MapLibre MarkerOptions documentation.

2. Custom markers

The most powerful option in MarkerOptions is element.

It allows you to replace the default blue pin with any DOM element — for example, an <img> tag, a styled <div>, or an inline <svg>.

This makes it easy to integrate custom icons, badges, or branded styles.

Example: using Geoapify Marker Icon API

The Geoapify Map Marker Icon API can generate PNG or SVG icons on the fly. You can pick an icon type, set a color, add a shadow, and adjust scaling — then plug the resulting image URL into a MapLibre marker.

// Generate a marker icon URL with Geoapify
function geoapifyIconUrl({ icon, iconType = "material", color = "#8b5cf6" }) {
  const url = new URL("https://api.geoapify.com/v2/icon/");
  url.searchParams.set("type", "awesome");
  url.searchParams.set("icon", icon);
  url.searchParams.set("iconType", iconType);
  url.searchParams.set("color", color);
  url.searchParams.set("shadow", "true");
  url.searchParams.set("scaleFactor", "2");
  url.searchParams.set("apiKey", GEOAPIFY_API_KEY);
  return url.toString();
}

// Create an <img> element for the marker
const img = document.createElement("img");
img.alt = "Restaurant marker";
img.src = geoapifyIconUrl({ icon: "restaurant" });

// Wrap it in a container div
const wrapper = document.createElement("div");
wrapper.className = "map-marker";
wrapper.appendChild(img);

// Create the marker
new maplibregl.Marker({ element: wrapper, anchor: "bottom", offset: [0, -4] })
  .setLngLat([lon, lat])
  .addTo(map);
Enter fullscreen mode Exit fullscreen mode

Here we use anchor: "bottom" and a small negative offset so that the tip of the pin icon aligns exactly with the map coordinates.

👉 Try this in the interactive demo:

Example: adjusting offset for custom icons

When you use a custom icon, the default anchor might not match the part of the icon you want to “touch” the map point. That’s where the offset option becomes important.

Case 1: square icon (40×40px)

If you have a round or square icon (e.g. 40×40px) and want it centered exactly on the latitude/longitude, no offset is needed:

const icon40 = document.createElement("img");
icon40.src = "marker-40x40.png";

new maplibregl.Marker({
  element: icon40,
  anchor: "center", // center aligns with lat/lon
  offset: [0, 0]
})
  .setLngLat([lon, lat])
  .addTo(map);
Enter fullscreen mode Exit fullscreen mode

Case 2: pin icon (31×46px with shadow 4px)
For a pin-shaped icon (31×46px, with a shadow extending 4px below), the “tip” of the pin should touch the coordinates.
Here we shift the marker upward so the bottom tip aligns with the point:

const pin = document.createElement("img");
pin.src = "marker-31x46-shadow4.png";

new maplibregl.Marker({
  element: pin,
  anchor: "bottom",       // attach by bottom edge
  offset: [0, -4]         // shift up by shadow size
})
  .setLngLat([lon, lat])
  .addTo(map);
Enter fullscreen mode Exit fullscreen mode

👉 The offset ensures that the visual tip of the marker sits exactly on the coordinates, rather than floating above or overlapping incorrectly.

3. Adding a popup to a marker

Markers in MapLibre can be paired with a Popup so that extra information appears when the user interacts with them.

Popups are not part of the marker itself, but you can attach them using the .setPopup() method.

Example: simple popup

const marker = new maplibregl.Marker()
  .setLngLat([lon, lat])
  .setPopup(
    new maplibregl.Popup({ offset: 25 })
      .setText("Hello from this location!")
  )
  .addTo(map);

// Open the popup immediately
marker.togglePopup();
Enter fullscreen mode Exit fullscreen mode

This creates a marker with a text popup that appears when clicked. In this case, we also call togglePopup() so it opens by default.

Custom popup content

Popups don’t have to be plain text — you can also provide custom HTML using .setHTML(). This makes it possible to render richer content, such as formatted addresses, buttons, or images.

const popup = new maplibregl.Popup({ offset: 25 })
  .setHTML(`
    <div class="popup-content">
      <h3>Restaurant</h3>
      <p>Open daily 10:00–22:00</p>
      <a href="https://example.com" target="_blank">More info</a>
    </div>
  `);

new maplibregl.Marker()
  .setLngLat([lon, lat])
  .setPopup(popup)
  .addTo(map);
Enter fullscreen mode Exit fullscreen mode

Known issue: event propagation

When you click a marker to open its popup, the click event also bubbles up to the map, which may trigger map.on("click", …) handlers.
To avoid unwanted side effects, check whether the click originated from a marker element before running map-level logic:

map.on("click", (event) => {
  // Ignore clicks on existing markers
  if (event.originalEvent.target.closest(".map-marker")) {
    return;
  }

  // ... 
});
Enter fullscreen mode Exit fullscreen mode

This way, clicking on a marker will only open its popup, without triggering your map’s click handler.

4. Marker events

Markers can respond to both DOM events (like clicks or hovers) and drag events if they are created with draggable: true.

DOM events

Attach using marker.getElement():

const marker = new maplibregl.Marker()
  .setLngLat([lon, lat])
  .addTo(map);

marker.getElement().addEventListener("click", () => {
  console.log("Marker clicked!");
});

marker.getElement().addEventListener("mouseenter", () => {
  marker.getElement().style.cursor = "pointer";
});
Enter fullscreen mode Exit fullscreen mode

This way, you can make markers interactive beyond just showing a popup.

Drag events

Available when the draggableoption is true,, the marker can be moved around. You can then listen to the drag lifecycle:

Event name When it fires
dragstart User starts dragging the marker
drag Marker is being dragged
dragend User releases the marker after dragging
const marker = new maplibregl.Marker({ draggable: true })
  .setLngLat([lon, lat])
  .addTo(map);

// Listen for drag events
marker.on("dragstart", () => console.log("Drag started"));
marker.on("drag", () => console.log("Dragging..."));
marker.on("dragend", () => {
  const lngLat = marker.getLngLat();
  console.log(`New position: ${lngLat.lng}, ${lngLat.lat}`);
});

Enter fullscreen mode Exit fullscreen mode

This is especially useful for “pick a location” UIs, where the user drags a pin to select an exact spot.

📖 See details in the MapLibre Marker docs.

5. Managing markers

When working with more than one marker, it’s good practice to keep track of them so you can update or remove them later.

Keep markers in an array

const markers = [];

function addMarker(lon, lat) {
  const marker = new maplibregl.Marker()
    .setLngLat([lon, lat])
    .addTo(map);

  markers.push(marker);
}
Enter fullscreen mode Exit fullscreen mode

Remove a marker

Each marker has a .remove() method:

markers[0].remove(); // removes the first marker from the map
Enter fullscreen mode Exit fullscreen mode

Remove all markers

markers.forEach(marker => marker.remove());
markers.length = 0; // clear the array
Enter fullscreen mode Exit fullscreen mode

Replace markers dynamically

This is useful when showing search results:

  1. Remove old markers.
  2. Add new ones from the latest data.

Keeping markers organized prevents memory leaks and avoids clutter when updating the map dynamically.

6. When not to use markers

Markers in MapLibre are DOM elements placed on top of the map. This makes them flexible and easy to customize, but also less efficient when you need to display hundreds or thousands of points. Each marker adds a new node to the DOM, which can slow down rendering and interactions.

When to avoid markers

  • Displaying large datasets (thousands of locations).
  • Real-time updates where many markers are added/removed frequently.
  • Use cases where performance and smooth zooming/panning are critical.

Alternative: Symbol layers

For bulk rendering, prefer a GeoJSON source + SymbolLayer.

This approach draws icons directly on the map canvas, which is much more efficient.

map.addSource("places", {
  type: "geojson",
  data: myGeoJson
});

map.addLayer({
  id: "places-layer",
  type: "symbol",
  source: "places",
  layout: {
    "icon-image": "marker-15",
    "icon-size": 1.5
  }
});
Enter fullscreen mode Exit fullscreen mode

Hybrid approach

  • Use symbol layers for the full dataset.
  • Use markers for selected or interactive points (e.g., user’s chosen location, currently active place).

8. FAQ

1. How do I change marker size on high-DPI (retina) displays?

Use a higher-resolution image (2× or SVG) and scale it down with CSS to keep it sharp.

2. Can I drag markers? How do I read their updated coordinates?

Yes — set draggable: true in MarkerOptions and listen to the dragend event. Call marker.getLngLat() to get the new position.

3. What’s the best way to show thousands of points?

Use a GeoJSON source + SymbolLayer, not individual markers.

4. How do I make markers clickable without interfering with the map?

Use marker.getElement().addEventListener("click", …) or attach a popup. In map click handlers, ignore clicks that come from marker elements using event.target.closest(".map-marker").

5. How do I automatically open a popup when a marker is created or added?

After calling .setPopup(), use marker.togglePopup() to open it right away.

6. Can I use inline SVG as a marker?

Yes — pass an element containing your SVG to MarkerOptions.element.

7. How do I rotate a marker with heading or bearing?

Use the rotation option. Combine with rotationAlignment: "map" to keep it aligned to map bearing.

8. Can I animate markers (bounce, pulse, fade)?

Yes — since markers are DOM elements, you can animate them with CSS transitions, keyframes, or libraries like GSAP.

9. How do I add text labels to markers?

Include a <span> or <div> inside your marker element and style it with CSS.

10. How do I cluster many markers?

Don’t cluster DOM markers directly. Instead, cluster your data in GeoJSON and use a SymbolLayer for clustered icons.

11. Can I change marker color dynamically after adding it?

Yes — update the marker’s DOM element (e.g., change an <img> src or CSS class).

12. How do I handle overlapping markers?

Use different offsets, z-index (marker.getElement().style.zIndex), or clustering.

13. How do I remove all markers at once?

Keep them in an array and loop through calling .remove() on each.

14. How do I make markers accessible?

Add alt text for images or aria-label for custom HTML. Use cursor: pointer for clickable markers and keep touch targets large enough on mobile.

Wrap-up

Markers in MapLibre GL are a flexible way to place interactive elements on the map. With MarkerOptions, you can fine-tune alignment, make them draggable, or replace the default pin with your own custom HTML or icons (for example, using the Geoapify Map Marker Icon API).

Use markers when you need:

  • Interactive pins that users can drag or click.
  • Custom icons, badges, or HTML content.
  • Highlighted or temporary overlays.

But remember:

  • For large datasets, prefer SymbolLayers with a GeoJSON source for performance.
  • Keep markers organized (arrays, cleanup) to avoid clutter and memory leaks.

In the next article, we’ll take this further and focus on popups — attaching them to markers, rendering custom HTML, and fetching live data (e.g., place details from Geoapify).

Top comments (0)