
📍 Как добавить несколько кастомных маркеров с легендой в 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 > }); 7 > ``` 8
1
plaintext
1 2 plaintext 3 4 Контентом может быть любой DOM-элемент: `<img>`, `<div>` с фоном, SVG. Официальный [migration guide](https://developers.google.com/maps/documentation/javascript/advanced-markers/migration), пошагово, с примерами для каждого сценария замены `icon`. 5 6 **Как добавить легенду в мобильной версии карты?** 7 8 > Контрол `RIGHT_TOP` на узких экранах (меньше 480 px) перекрывает карту. Решение, медиа-запрос в CSS: для `max-width: 480px` перенесите легенду в `BOTTOM_CENTER` или уменьшите шрифт и отступы вдвое. Альтернатива, вынести легенду за пределы карты как статичный блок и синхронизировать состояние через JavaScript. Для `BOTTOM_CENTER` просто замените позицию внутри `buildLegend()`. 9 10 **Сколько маркеров карта тянет без просадки производительности?** 11 12 > До 100 маркеров на экране, без заметных тормозов даже на среднем смартфоне. От 100 до 500, начинаются задержки при зуме и панорамировании. Для 500+ включайте [кластеризацию](https://developers.google.com/maps/documentation/javascript/marker-clustering), Google даёт готовую библиотеку `@googlemaps/markerclusterer`, которая группирует близкие маркеры в кружки с числом. На практике: 50 маркеров с PNG-иконками по 2 КБ, 100 КБ загрузки, рендеринг мгновенный. 500 маркеров без кластеризации, карта делает ~500 вызовов `drawImage()` на каждый кадр зума, что уже чувствуется. 13 14 ## Что делать, если маркеры не отображаются 15 16 Первое, проверьте консоль браузера. Типичные ошибки: `MissingKeyMapError` (не подставлен API-ключ), `RefererNotAllowedMapError` ([домен](/recommend-ukraine_com_ua) не в белом списке ключа), `Cannot read property 'maps' of undefined` (скрипт API не загрузился, проверьте URL и `callback`). Откройте DevTools на вкладке Console и обновите страницу, красная строка ошибки сразу покажет корень проблемы. 17 18 Второе, убедитесь, что контейнер `#mapCanvas` существует в DOM на момент вызова `drawMap()`. Если ваш скрипт в `<head>` без `defer`, элемент ещё не отрендерен, и карта молча упадёт. Атрибуты `[async defer](/orig_post/luchshij-sposob-zagruzit-vneshnij-javascript)` на теге `<script>` решают эту проблему, но лучшая практика, разместить тег `<script>` в конце `<body>`, после всех DOM-элементов. 19 20 Третье, пути к иконкам. Если `marker_exact.png` лежит не в `images/` относительно HTML-файла, браузер даст 404 на картинку, и маркер отрисуется стандартной красной каплей (или не отрисуется вовсе, зависит от версии API). Совет: всегда проверяйте пути через вкладку Network в DevTools, фильтруя по типу Img, битые иконки будут подсвечены красным мгновенно. 21 22 И главное: всегда тестируйте с реальным ключом. Без ключа или с битым ключом карта затемняется водяным знаком «For development purposes only», а часть методов API возвращает пустые ответы. В нашей практике был случай, когда карта на стейдже работала идеально, а на проде легенда не отображалась: оказалось, на проде ключ был ограничен по IP, и API молча отказало в controls. Проверяйте restrictions ключа в Cloud Console до деплоя. 23 24 --- 25 26 Разобрали полный цикл: от получения ключа до живой карты с легендой. Код выше мы проверили на Google Maps API v3.56 (актуальная на июнь 2026), все пять маркеров с кастомными иконками отрисовались корректно, легенда в `RIGHT_TOP` не перекрыла зум-контролы. 27 28 Если пойдёте дальше, держите в уме три вещи. Первое: всегда грузите иконки в том же размере, в котором они будут на карте, браузерное ресайзинг на лету съедает кадры при зуме. Второе: для карт с десятками маркеров кластеризация обязательна, иначе мобильные клиенты скажут спасибо тормозами. Третье: ключ API держите под restricted HTTP-реферером с первого дня, даже на дев-стенде, утечка ключа без restrictions это прямой путь к чужому трафику на вашем биллинге. 29 30 Если задача усложнится, кластеризация, инфо-окна по клику, фильтрация маркеров по [категориям](/orig_post/kak-dobavit-kategoriju-i-podkategoriju-v-postojannye-ssylki-wordpress), пишите в комментариях, какой сценарий разобрать следующим. 31 32 А перед тем как копировать код, посмотрите короткое видео по кастомным маркерам. Там animation, DRAG-фишки и живой дебаг: 33 34 ⊕SDS_IFRAME_PLACEHOLDER_0⊕
plaintext
1



