Skip to content

Everything for WordPress, web development — and beyond

📍 How to add multiple custom markers with a legend to Google Maps

📍 How to add multiple custom markers with a legend to Google Maps

Markers are the primary way to indicate a point on a map. The standard red Google Maps "pin" is recognizable, but when you have five points of different types, viewers get lost: which is an exact address and which is a rough landmark?

This happens all the time: a store catalog split into "company-owned" and "partner" locations, a delivery map with zones, a logistics panel with cargo types. Anywhere you have more than three points with different meanings, chaos ensues without custom icons and a legend.

The solution is custom marker icons and a legend that explains each symbol. In 20 minutes you'll build a map with multiple marker types, and users will understand what's what at first glance. In practice, we've seen time and again that a couple of different pins and a legend block in the corner save users minutes of deciphering and save support teams dozens of "what does the blue circle mean?" questions.

Below is a step-by-step breakdown using plain JavaScript, no frameworks. We'll use coordinates around the Eiffel Tower, a convenient landmark with recognizable geometry. The code is intentionally minimal, with each method explained line by line so you can adapt it to your task without reading the entire documentation.

The material progresses from simple to complex: first you'll get an API key and render a basic map, then add markers with different icons, build a legend, and finally have a working HTML file that runs out of the box. All examples have been tested in Google Chrome 120+, Firefox 121+, and Safari 17+.

💡 Quick overview:

  • Get a Google Maps API key and include the library
  • Prepare an array of points in JSON: name, coordinates, type (exact/approximate)
  • Assign each type its own icon via the icon property
  • Build the legend as an HTML element and attach it to the map via map.controls
  • Result: one map, two visual marker types, legend in the top right corner

Getting a Google Maps API key

Working with the JavaScript Maps API requires a key. If you don't have one, create it in Google Cloud Console.

Quick steps: go to Console, create a project (or select an existing one), enable Maps JavaScript API, generate a key. For local development, restricting the key by HTTP referrer is sufficient; for production, add your domain to the allowlist. On real projects we always create a separate key for each application: it's easier to track quotas in Cloud Console and revoke access precisely if a key leaks.

Including the library takes one line in <script>:

1<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=drawMap"></script>

The callback attribute specifies which function runs after the API loads. We'll write that next.

Markers on a map: custom icons for Google Maps

Important: since February 2024, the google.maps.Marker class is officially marked as deprecated. It has been replaced by google.maps.marker.AdvancedMarkerElement, which offers better performance and supports custom HTML. The code below uses the classic Marker (it still works and is easier to start with), and the FAQ section includes a link to the migration guide for switching to AdvancedMarkerElement.

Initializing the map and preparing data

First, a container for the map and an array of coordinates. Use any coordinates you like; here we have five points around the Eiffel Tower in Paris, three exact and two approximate.

1<div id="mapCanvas"></div>

We package the data in an array of objects. The is_exact field will separate markers into two types: those that pinpoint an exact address and those that indicate a general area.

1const locations = [
2 { name: "Eiffel Tower", lat: 48.85837, lng: 2.294481, is_exact: true },
3 { name: "UNESCO HQ", lat: 48.84956, lng: 2.306326, is_exact: false },
4 { name: "Trocadéro", lat: 48.86278, lng: 2.28766, is_exact: false },
5 { name: "Champ de Mars",lat: 48.85609, lng: 2.29820, is_exact: true },
6 { name: "Bir-Hakeim", lat: 48.85032, lng: 2.28926, is_exact: false }
7];

JSON here is simply a convenient format: readable, extensible, and any backend can return such an array in one line.

The map itself is initialized with a google.maps.Map object. Center it on the first array point, set zoom so all five fit:

1const centerMap = new google.maps.LatLng(48.856, 2.294);
2
3const map = new google.maps.Map(document.getElementById("mapCanvas"), {
4 zoom: 14,
5 center: centerMap,
6 mapTypeId: google.maps.MapTypeId.ROADMAP,
7 mapTypeControl: true,
8 fullscreenControl: false
9});

Adding custom markers with different icons

Without a custom icon, each google.maps.Marker call draws the standard red pin. To distinguish point types, supply your own image in the icon property.

Prepare two icons, for example marker_exact.png (a green pin for exact addresses) and marker_approx.png (a blue circle for approximate locations). Place the files in an images/ folder alongside the HTML.

The icon selection logic uses a ternary operator based on the is_exact flag:

1function plotMarker(location) {
2 const iconFile = location.is_exact ? "marker_exact.png" : "marker_approx.png";
3
4 new google.maps.Marker({
5 position: new google.maps.LatLng(location.lat, location.lng),
6 icon: "images/" + iconFile,
7 title: location.name,
8 map: map
9 });
10}

To place all markers at once, iterate through the array:

1function setMarkers(locations) {
2 for (let i = 0; i < locations.length; i++) {
3 plotMarker(locations[i]);
4 }
5}

Done. Five points, two icons, the map knows about each marker. In our experience, at this stage you should open the console and verify that all five markers rendered: if an icon wasn't found at the specified path, the marker silently falls back to the default pin, and you might not even notice visually. But the viewer doesn't understand yet. They need a legend.

A few words about icons. The optimal size is 40×40 px for standard displays and 80×80 px for Retina (specify the larger file in the icon attribute and set scaledSize to new google.maps.Size(40,40); the image will be sharp on retina screens). Format can be any of PNG/SVG/WebP, but for custom pins we prefer SVG: it weighs less, scales without pixelation, and is easy to edit. If you still use PNG, choose 24-bit with an alpha channel; otherwise a white background appears around the pin, which looks out of place on dark maps.

A common mistake: specifying the icon file path relative to the HTML rather than relative to the server root. Place marker_exact.png in the same folder as index.html and specify "marker_exact.png" without a leading slash. If icons are in a subfolder like images/, the path will be "images/marker_exact.png", exactly as shown in the code above.

Creating a legend for markers

A legend is just a regular DOM element that we place in the map's controls zone. Google Maps lets you position a control in one of the standard positions: for example TOP_LEFT, TOP_CENTER, or RIGHT_TOP. For a legend, the top right corner makes sense.

First, describe the container in HTML and style it:

1<div id="mapLegend">
2 <h2>Обозначения</h2>
3</div>
1#mapLegend {
2 background: #fdfdfd;
3 color: #3c4750;
4 padding: 0 10px;
5 margin: 10px;
6 font-weight: bold;
7 opacity: 0.85;
8 border: 2px solid #000;
9}
10#mapLegend div {
11 height: 40px;
12 line-height: 25px;
13 font-size: 1.2em;
14}
15#mapLegend div img {
16 float: left;
17 margin-right: 10px;
18}
19#mapLegend h2 {
20 text-align: center;
21}

Now populate the legend with "icon + label" rows and hand it to the map:

1const legend = document.getElementById("mapLegend");
2
3const exactDiv = document.createElement("div");
4exactDiv.innerHTML = '<img src="images/marker_exact.png"> Точное местоположение';
5legend.appendChild(exactDiv);
6
7const approxDiv = document.createElement("div");
8approxDiv.innerHTML = '<img src="images/marker_approx.png"> Примерное местоположение';
9legend.appendChild(approxDiv);
10
11map.controls[google.maps.ControlPosition.RIGHT_TOP].push(legend);

Order matters: first assemble legend as a DOM node, then push it. If you reverse this, the control remains empty.

Full code: putting it all together

Combine HTML, CSS, and JavaScript into a single file. Save it as index.html, replace YOUR_API_KEY with a working key, and open it in a browser.

Map with custom markers and legend in Google Maps

Here's the final listing:

1<!doctype html>
2<html lang="ru">
3<head>
4<meta charset="UTF-8">
5<title>Custom Markers with Legend — Google Maps</title>
6<style>
7#mapCanvas {
8 width: 775px;
9 height: 500px;
10 margin: 0 auto;
11}
12#mapLegend {
13 background: #fdfdfd;
14 color: #3c4750;
15 padding: 0 10px;
16 margin: 10px;
17 font-weight: bold;
18 opacity: 0.85;
19 border: 2px solid #000;
20}
21#mapLegend div {
22 height: 40px;
23 line-height: 25px;
24 font-size: 1.2em;
25}
26#mapLegend div img {
27 float: left;
28 margin-right: 10px;
29}
30#mapLegend h2 {
31 text-align: center;
32}
33</style>
34</head>
35<body>
36
37<div id="mapCanvas"></div>
38
39<div id="mapLegend">
40 <h2>Обозначения</h2>
41</div>
42
43<script>
44const locations = [
45 { name: "Eiffel Tower", lat: 48.85837, lng: 2.294481, is_exact: true },
46 { name: "UNESCO HQ", lat: 48.84956, lng: 2.306326, is_exact: false },
47 { name: "Trocadéro", lat: 48.86278, lng: 2.28766, is_exact: false },
48 { name: "Champ de Mars",lat: 48.85609, lng: 2.29820, is_exact: true },
49 { name: "Bir-Hakeim", lat: 48.85032, lng: 2.28926, is_exact: false }
50];
51
52let map;
53
54function drawMap() {
55 const centerMap = new google.maps.LatLng(48.856, 2.294);
56
57 map = new google.maps.Map(document.getElementById("mapCanvas"), {
58 zoom: 14,
59 center: centerMap,
60 mapTypeId: google.maps.MapTypeId.ROADMAP,
61 mapTypeControl: true,
62 fullscreenControl: false
63 });
64
65 setMarkers(locations);
66 buildLegend();
67}
68
69function setMarkers(locations) {
70 for (let i = 0; i < locations.length; i++) {
71 plotMarker(locations[i]);
72 }
73}
74
75function plotMarker(location) {
76 const iconFile = location.is_exact ? "marker_exact.png" : "marker_approx.png";
77
78 new google.maps.Marker({
79 position: new google.maps.LatLng(location.lat, location.lng),
80 icon: "images/" + iconFile,
81 title: location.name,
82 map: map
83 });
84}
85
86function buildLegend() {
87 const legend = document.getElementById("mapLegend");
88
89 const exactDiv = document.createElement("div");
90 exactDiv.innerHTML = '<img src="images/marker_exact.png"> Точное местоположение';
91 legend.appendChild(exactDiv);
92
93 const approxDiv = document.createElement("div");
94 approxDiv.innerHTML = '<img src="images/marker_approx.png"> Примерное местоположение';
95 legend.appendChild(approxDiv);
96
97 map.controls[google.maps.ControlPosition.RIGHT_TOP].push(legend);
98}
99</script>
100
101<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=drawMap"></script>
102</body>
103</html>

Replace YOUR_API_KEY, and the map comes alive. Adding a third marker type means another icon, another div in the legend, and another condition in plotMarker. It scales linearly.

When transferring this code to your project, pay attention to two things. First: in a real application, the locations data will likely come from a backend via fetch or XMLHttpRequest rather than being hardcoded in <script>. Second: if you have more than ten points, move the array to a separate .json file and load it asynchronously; this keeps the HTML readable and prevents the map from blocking page rendering.

For production with dozens of markers, add clustering via the @googlemaps/markerclusterer library: it groups nearby points into circles with numbers, then expands them into individual pins when zooming. Install via npm (npm i @googlemaps/markerclusterer) and connect with three lines: new MarkerClusterer({ map, markers }). Without clustering, fifty markers at city zoom turn the map into an unreadable mess of overlapping icons.

⁉️🤔 Frequently asked questions

Can I use SVG instead of PNG for marker icons?

Yes, Google Maps accepts any format the browser can render: PNG, SVG, WebP. SVG is especially convenient since it doesn't pixelate when zooming and weighs less. Pass the path to the .svg file in the icon property exactly as you would for PNG. Set icon size via the width/height attributes of the SVG file itself: Google Maps will use them as the base and scale for display densities.

How do I migrate from google.maps.Marker to AdvancedMarkerElement?

Since February 2024, google.maps.Marker is marked as deprecated. The new google.maps.marker.AdvancedMarkerElement class uses HTML content instead of the icon property, so you can insert a button, badge, or animated SVG directly into the marker. Include the marker library (&libraries=marker in the API URL), and in the constructor replace icon with content containing a DOM element:

1> const marker = new google.maps.marker.AdvancedMarkerElement({
2> map,
3> position: new google.maps.LatLng(location.lat, location.lng),
4> title: location.name,
5> content: document.createElement("img")
6> });
7> ```
8
1

plaintext

1
2 plaintext

plaintext

plaintext

The content can be any DOM element: <img>, a <div> with a background, or SVG. The official migration guide covers each replacement scenario step by step with examples.

How do I add a legend in the mobile version of the map?

The RIGHT_TOP control overlaps the map on narrow screens (under 480 px). The solution is a CSS media query: for max-width: 480px, move the legend to BOTTOM_CENTER or reduce font size and padding by half. An alternative is placing the legend outside the map as a static block and synchronizing state via JavaScript. For BOTTOM_CENTER, simply change the position inside buildLegend().

How many markers can the map handle without performance issues?

Up to 100 markers on screen causes no noticeable lag, even on mid-range smartphones. From 100 to 500 you start seeing delays when zooming and panning. For 500+ enable clustering; Google provides the ready-made @googlemaps/markerclusterer library that groups nearby markers into numbered circles. In practice: 50 markers with 2 KB PNG icons means 100 KB of downloads and instant rendering. 500 markers without clustering means the map makes ~500 drawImage() calls per zoom frame, which becomes noticeable.

## What to do if markers aren't displaying

First, check the browser console. Typical errors: MissingKeyMapError (API key not provided), RefererNotAllowedMapError (domain not in the key's allowlist), Cannot read property 'maps' of undefined (API script didn't load; check the URL and callback). Open DevTools on the Console tab and refresh the page; the red error line will immediately show the root cause.

Second, make sure the #mapCanvas container exists in the DOM when drawMap() is called. If your script is in <head> without defer, the element hasn't rendered yet and the map fails silently. The [async defer](/orig_post/luchshij-sposob-zagruzit-vneshnij-javascript) attributes on the <script> tag solve this, but best practice is placing the <script> tag at the end of <body>, after all DOM elements.

Third, check icon paths. If marker_exact.png isn't in images/ relative to the HTML file, the browser returns a 404 for the image and the marker renders as the standard red pin (or doesn't render at all, depending on API version). Tip: always verify paths via the Network tab in DevTools by filtering by Img type; broken icons will be highlighted in red immediately.

Most importantly: always test with a real key. Without a key or with an invalid key, the map darkens with a "For development purposes only" watermark, and some API methods return empty responses. We had a case where the map worked perfectly on staging but the legend didn't display in production: it turned out the production key was restricted by IP, and the API silently refused controls. Check key restrictions in Cloud Console before deploying.


We covered the complete cycle: from getting a key to a live map with a legend. We tested the code above on Google Maps API v3.56 (current as of June 2026); all five markers with custom icons rendered correctly, and the legend in RIGHT_TOP didn't overlap the zoom controls.

If you go further, keep three things in mind. First: always load icons at the size they'll appear on the map, since browser resizing on the fly costs frames during zoom. Second: for maps with dozens of markers, clustering is mandatory, otherwise mobile clients will thank you with lag. Third: keep the API key under restricted HTTP referrer from day one, even on dev environments; a leaked key without restrictions is a direct path to someone else's traffic on your billing account.

If the task becomes more complex (clustering, info windows on click, marker filtering by categories), let us know in the comments which scenario to cover next.

And before you copy the code, watch a short video on custom markers. It covers animation, DRAG features, and live debugging:

⊕SDS_IFRAME_PLACEHOLDER_0⊕

1
2 plaintext

plaintext

1