
📍 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
iconproperty - 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.

Important: since February 2024, the
google.maps.Markerclass is officially marked as deprecated. It has been replaced bygoogle.maps.marker.AdvancedMarkerElement, which offers better performance and supports custom HTML. The code below uses the classicMarker(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.
1 const 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:
1 const centerMap = new google.maps.LatLng(48.856, 2.294); 2 3 const 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:
1 function 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:
1 function 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:
1 const legend = document.getElementById("mapLegend"); 2 3 const exactDiv = document.createElement("div"); 4 exactDiv.innerHTML = '<img src="images/marker_exact.png"> Точное местоположение'; 5 legend.appendChild(exactDiv); 6 7 const approxDiv = document.createElement("div"); 8 approxDiv.innerHTML = '<img src="images/marker_approx.png"> Примерное местоположение'; 9 legend.appendChild(approxDiv); 10 11 map.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.

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> 44 const 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 52 let map; 53 54 function 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 69 function setMarkers(locations) { 70 for (let i = 0; i < locations.length; i++) { 71 plotMarker(locations[i]); 72 } 73 } 74 75 function 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 86 function 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
.svgfile in theiconproperty exactly as you would for PNG. Set icon size via thewidth/heightattributes 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.Markeris marked as deprecated. The newgoogle.maps.marker.AdvancedMarkerElementclass uses HTML content instead of theiconproperty, so you can insert a button, badge, or animated SVG directly into the marker. Include the marker library (&libraries=markerin the API URL), and in the constructor replaceiconwithcontentcontaining 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_TOPcontrol overlaps the map on narrow screens (under 480 px). The solution is a CSS media query: formax-width: 480px, move the legend toBOTTOM_CENTERor 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. ForBOTTOM_CENTER, simply change the position insidebuildLegend().
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/markerclustererlibrary 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 ~500drawImage()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



