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:
Use HTML-based markers to place a single icon on the map
👉 Live example on JSFiddleUse a GeoJSON data layer to add and style multiple markers
👉 Live example on JSFiddle
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
- Step 2: Add a Single Custom Marker
- Step 3: Add a Layer of Custom Markers
- Step 4: Summary and Demo Links
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>
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
});
🗝️ 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,
};
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";
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})`;
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";
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`;
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],
})
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])
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")
)
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);
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.
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);
});
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 });
}
);
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 = "";
});
}
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.
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
- MapLibre GL JS
- Geoapify Marker Icon API
- Geoapify Places API
- Get your Geoapify API key
- Marker Icon Playground
Have questions or ideas for extending this example (e.g., clustering or filters)? Drop a comment or fork the JSFiddle!
Top comments (0)