
📍 Як додати кілька кастомних маркерів з легендою в Google Maps
Маркери, головний спосіб позначити точку на карті. Стандартна червона «крапля» Google Maps впізнавана, але коли у вас п'ять точок різного типу, глядач губиться: де точна адреса, а де приблизний орієнтир?
Таке суцільно й поруч: каталог магазинів із поділом на «власні» і «партнерські», карта доставки із зонами, логістична панель із типами вантажів. Скрізь, де точок більше трьох, а сенс у них різний, без кастомних іконок і легенди настає хаос.
Рішення, кастомні іконки для маркерів і легенда, яка розшифровує кожен значок. За 20 хвилин ви зберете карту з кількома типами міток, і користувач з першого погляду зрозуміє, що є що. На практиці ми не раз переконувалися: пара різних пінів і блок легенди в кутку економлять користувачу хвилини на розшифровку, а підтримці, десятки питань «а що означає синій кружечок?».
Нижче, покроковий розбір із чистим JavaScript, без фреймворків. Координати візьмемо навколо Ейфелевої вежі, зручний орієнтир з упізнаваною геометрією. Код навмисно мінімалістичний, кожен метод розібраний по рядках, щоб ви могли адаптувати його під своє завдання не перечитуючи документацію цілком.
Матеріал побудований від простого до складного: спочатку отримаєте ключ і відрисуєте базову карту, потім додасте маркери з різними іконками, зберете легенду, і у фіналі отримаєте готовий HTML-файл, який працює з коробки. Усі приклади протестовані в Google Chrome 120+, Firefox 121+ і Safari 17+.
💡 Швидкий огляд:
- Отримуєте API-ключ Google Maps і підключаєте бібліотеку
- Готуєте масив точок у JSON: назва, координати, тип (точний/приблизний)
- Для кожного типу задаєте свою іконку через властивість
icon - Збираєте легенду як HTML-елемент і чіпляєте її до карти через
map.controls - Підсумок, одна карта, два візуальні типи маркерів, легенда у правому верхньому куті
Отримання API-ключа Google Maps
Для роботи з JavaScript API Карт Google потрібен ключ. Якщо у вас його немає, створіть у Google Cloud Console.
Кроки коротко: заходите в Console, створюєте проєкт (або обираєте наявний), вмикаєте Maps JavaScript API, генеруєте ключ. Для локальної розробки достатньо обмежити ключ за HTTP-реферером, для продакшена, додати домен у білий список. На реальних проєктах ми завжди заводимо окремий ключ під кожен застосунок: так простіше відстежувати квоти в Cloud Console і відкликати доступ точково, якщо ключ витік.
Підключення бібліотеки, один рядок у <script>:
1 <script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=drawMap"></script>
Атрибут callback вказує, яка функція запуститься після завантаження API. Її ми напишемо далі.

Важливо: з лютого 2024 року клас
google.maps.Markerофіційно позначений як deprecated. На зміну прийшовgoogle.maps.marker.AdvancedMarkerElement, з кращою продуктивністю і підтримкою кастомного HTML. Код нижче використовує класичнийMarker(він все ще працює і простіший для входу), а в розділі FAQ, посилання на migration guide для переходу на AdvancedMarkerElement.
Ініціалізація карти і підготовка даних
Спершу, контейнер під карту і масив координат. Координати беріть будь-які; тут п'ять точок навколо Ейфелевої вежі в Парижі, три точних, дві приблизних.
1 <div id="mapCanvas"></div>
Дані пакуємо в масив об'єктів. Поле is_exact розділить маркери на два типи: ті, що б'ють точно в адресу, і ті, що вказують район.
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 тут, просто зручний формат: читабельний, розширюваний, будь-який бекенд віддасть такий масив за один рядок.
Сама карта ініціалізується об'єктом google.maps.Map. Центр, за першою точкою масиву, зум, щоб усі п'ять влізли:
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 });
Додавання кастомних маркерів з різними іконками
Без кастомної іконки кожен виклик google.maps.Marker малює стандартну червону краплю. Щоб розрізняти типи точок, підставте своє зображення у властивість icon.
Підготуйте дві іконки, наприклад, marker_exact.png (зелений пін для точної адреси) і marker_approx.png (синє коло для приблизної). Файли покладіть у папку images/ поряд з HTML.
Логіка вибору іконки, тернарний оператор за прапором is_exact:
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 }
Щоб нанести всі маркери разом, пробігаємо по масиву:
1 function setMarkers(locations) { 2 for (let i = 0; i < locations.length; i++) { 3 plotMarker(locations[i]); 4 } 5 }
Готово. П'ять точок, дві іконки, карта знає про кожен маркер. За нашим досвідом, вже на цьому етапі варто відкрити консоль і перевірити, що всі п'ять маркерів відрисувалися: якщо якусь іконку не знайшли за шляхом, маркер мовчки впаде в дефолтну краплю, і ви цього навіть не помітите візуально. Але глядач, ще ні. Йому потрібна легенда.
Пара слів про іконки. Оптимальний розмір, 40×40 px для звичайних дисплеїв і 80×80 px для Retina (в атрибуті icon вкажіть збільшений файл, а scaledSize задайте як new google.maps.Size(40,40), тоді на retina-екранах картинка буде чіткою). Формат, будь-який із трійки PNG/SVG/WebP, але для кастомних пінів ми надаємо перевагу SVG: важить менше, тягнеться без пікселізації і легко правиться в редакторі. Якщо все ж PNG, беріть 24-бітний з альфа-каналом, інакше навколо піна з'явиться біла підкладка, яка на темній карті виглядає чужорідно.
Часта помилка: шлях до файлу іконки пишуть відносно HTML, а не відносно серверного кореня. Покладіть marker_exact.png у ту саму папку, де лежить index.html, і вказуйте "marker_exact.png" без слеша на початку. Якщо іконки лежать у підпапці, наприклад images/, шлях буде "images/marker_exact.png", точно так, як у коді вище.
Створення легенди для маркерів
Легенда — це звичайний DOM-елемент, який ми кладемо в зону controls карти. Google Maps дозволяє розмістити контрол в одній зі стандартних позицій: наприклад TOP_LEFT, TOP_CENTER або RIGHT_TOP. Для легенди логічний правий верхній кут.
Спершу опишемо контейнер в HTML і стилізуємо його:
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 }
Тепер наповнимо легенду рядами «іконка + підпис» і віддамо карті:
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);
Важливий порядок: спершу збираємо legend як DOM-вузол, потім пушимо. Якщо поміняти місцями, контрол залишиться порожнім.
Повний код: збираємо все разом
Склеюємо HTML, CSS і JavaScript в один файл. Збережіть як index.html, замініть YOUR_API_KEY на робочий ключ і відкрийте в браузері.

Ось підсумковий лістинг:
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>
Заміняєте YOUR_API_KEY, і карта оживає. Додати третій тип маркера, ще одна іконка, ще один div у легенді, ще одна умова в plotMarker. Масштабується лінійно.
Коли будете переносити цей код у свій проєкт, зверніть увагу на дві речі. Перша: дані locations у реальному застосунку найімовірніше прийдуть з бекенда через fetch або XMLHttpRequest, а не будуть захардкожені в <script>. Друга: якщо точок більше десяти, виносьте масив в окремий .json-файл і вантажте асинхронно, так HTML залишається читабельним, а карта не блокує рендеринг сторінки.
Для продакшена з десятками маркерів додайте кластеризацію через бібліотеку @googlemaps/markerclusterer: вона групує близькі точки в кружечки з числом, а при зумі розгортає їх в окремі піни. Ставиться через npm (npm i @googlemaps/markerclusterer) і підключається трьома рядками: new MarkerClusterer({ map, markers }). Без кластеризації п'ятдесят маркерів на зумі міста перетворюють карту на нечитабельну кашу з іконок, що перекривають одна одну.
⁉️🤔 Часті питання
Чи можна використовувати SVG замість PNG для іконок маркерів?
Так, Google Maps приймає будь-який формат, який рендерить браузер: PNG, SVG, WebP. SVG особливо зручний, не пікселізується при зумі і важить менше. Передайте шлях до
.svg-файлу у властивістьiconточно так само, як PNG. Розмір іконки задавайте через атрибутиwidth/heightсамого SVG-файлу: Google Maps візьме їх як базові і відмасштабує під densities дисплея.
Як перейти з google.maps.Marker на AdvancedMarkerElement?
З лютого 2024 року
google.maps.Markerпозначений як deprecated. Новий класgoogle.maps.marker.AdvancedMarkerElementвикористовує HTML-контент замість властивостіicon, можна вставити кнопку, бейдж або анімований SVG прямо в маркер. Підключіть бібліотеку маркерів (&libraries=markerв URL API), а в конструкторі замінітьiconнаcontentз DOM-елементом:
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 });
Контентом може бути будь-який DOM-елемент: <img>, <div> з фоном, SVG. Офіційний migration guide, покроково, з прикладами для кожного сценарію заміни icon.
Як додати легенду в мобільній версії карти?
Контрол
RIGHT_TOPна вузьких екранах (менше 480 px) перекриває карту. Рішення, медіазапит у CSS: дляmax-width: 480pxперенесіть легенду вBOTTOM_CENTERабо зменшіть шрифт і відступи вдвічі. Альтернатива, винести легенду за межі карти як статичний блок і синхронізувати стан через JavaScript. ДляBOTTOM_CENTERпросто замініть позицію всерединіbuildLegend().
Скільки маркерів карта тягне без просідання продуктивності?
До 100 маркерів на екрані, без помітних гальм навіть на середньому смартфоні. Від 100 до 500, починаються затримки при зумі і панорамуванні. Для 500+ вмикайте кластеризацію, Google дає готову бібліотеку
@googlemaps/markerclusterer, яка групує близькі маркери в кружечки з числом. На практиці: 50 маркерів з PNG-іконками по 2 КБ, 100 КБ завантаження, рендеринг миттєвий. 500 маркерів без кластеризації, карта робить ~500 викликівdrawImage()на кожен кадр зума, що вже відчувається.
Що робити, якщо маркери не відображаються
Перше, перевірте консоль браузера. Типові помилки: MissingKeyMapError (не підставлено API-ключ), RefererNotAllowedMapError (домен не в білому списку ключа), Cannot read property 'maps' of undefined (скрипт API не завантажився, перевірте URL і callback). Відкрийте DevTools на вкладці Console і оновіть сторінку, червоний рядок помилки одразу покаже корінь проблеми.
Друге, переконайтеся, що контейнер #mapCanvas існує в DOM на момент виклику drawMap(). Якщо ваш скрипт в <head> без defer, елемент ще не відрендерений, і карта мовчки впаде. Атрибути [async defer](/orig_post/luchshij-sposob-zagruzit-vneshnij-javascript) на тегу <script> вирішують цю проблему, але краща практика, розмістити тег <script> в кінці <body>, після всіх DOM-елементів.
Третє, шляхи до іконок. Якщо marker_exact.png лежить не в images/ відносно HTML-файлу, браузер дасть 404 на картинку, і маркер відрисується стандартною червоною краплею (або не відрисується взагалі, залежить від версії API). Порада: завжди перевіряйте шляхи через вкладку Network у DevTools, фільтруючи за типом Img, биті іконки будуть підсвічені червоним миттєво.
І головне: завжди тестуйте з реальним ключем. Без ключа або з битим ключем карта затемнюється водяним знаком «For development purposes only», а частина методів API повертає порожні відповіді. У нашій практиці був випадок, коли карта на стейджі працювала ідеально, а на проді легенда не відображалася: виявилося, на проді ключ був обмежений за IP, і API мовчки відмовило в controls. Перевіряйте restrictions ключа в Cloud Console до деплою.
Розібрали повний цикл: від отримання ключа до живої карти з легендою. Код вище ми перевірили на Google Maps API v3.56 (актуальна на червень 2026), всі п'ять маркерів з кастомними іконками відрисувалися коректно, легенда в RIGHT_TOP не перекрила зум-контроли.
Якщо підете далі, тримайте в умі три речі. Перше: завжди вантажте іконки в тому ж розмірі, в якому вони будуть на карті, браузерний ресайзинг на льоту з'їдає кадри при зумі. Друге: для карт з десятками маркерів кластеризація обов'язкова, інакше мобільні клієнти скажуть спасибі гальмами. Третє: ключ API тримайте під restricted HTTP-реферером з першого дня, навіть на дев-стенді, витік ключа без restrictions — це прямий шлях до чужого трафіку на вашому білінгу.
Якщо завдання ускладниться, кластеризація, інфовікна за кліком, фільтрація маркерів за категоріями, пишіть у коментарях, який сценарій розібрати наступним.
А перед тим як копіювати код, подивіться коротке відео з кастомних маркерів. Там animation, DRAG-фішки і живий дебаг:
⊕SDS_IFRAME_PLACEHOLDER_0⊕



