Skip to content

Всё для WordPress, веб-разработки — и не только

📍 Как добавить несколько кастомных маркеров с легендой в Google Maps

📍 Как добавить несколько кастомных маркеров с легендой в 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. Её мы напишем дальше.

Маркеры на карте: кастомные иконки для Google Maps

Важно: с февраля 2024 года класс google.maps.Marker официально помечен как deprecated. На смену пришёл google.maps.marker.AdvancedMarkerElement, с лучшей производительностью и поддержкой кастомного HTML. Код ниже использует классический Marker (он всё ещё работает и проще для входа), а в разделе FAQ, ссылка на migration guide для перехода на AdvancedMarkerElement.

Инициализация карты и подготовка данных

Сперва, контейнер под карту и массив координат. Координаты берите любые; здесь пять точек вокруг Эйфелевой башни в Париже, три точных, два примерных.

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

Данные упаковываем в массив объектов. Поле is_exact разделит маркеры на два типа: те, что бьют точно в адрес, и те, что указывают район.

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 здесь, просто удобный формат: читаемый, расширяемый, любой бэкенд отдаст такой массив за одну строчку.

Сама карта инициализируется объектом google.maps.Map. Центр, по первой точке массива, зум, чтобы все пять влезли:

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});

Добавление кастомных маркеров с разными иконками

Без кастомной иконки каждый вызов google.maps.Marker рисует стандартную красную каплю. Чтобы различать типы точек, подставьте своё изображение в свойство icon.

Подготовьте две иконки, например, marker_exact.png (зелёный пин для точного адреса) и marker_approx.png (синий круг для примерного). Файлы положите в папку images/ рядом с HTML.

Логика выбора иконки, тернарный оператор по флагу is_exact:

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}

Чтобы нанести все маркеры разом, пробегаем по массиву:

1function 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}

Теперь наполним легенду рядами «иконка + подпись» и отдадим карте:

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);

Важен порядок: сперва собираем legend как DOM-узел, потом пушим. Если поменять местами, контрол останется пустым.

Полный код: собираем всё вместе

Склеиваем HTML, CSS и JavaScript в один файл. Сохраните как index.html, замените YOUR_API_KEY на рабочий ключ и откройте в браузере.

Карта с кастомными маркерами и легендой в Google Maps

Вот итоговый листинг:

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>

Заменяете 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&#35;&#35; Что делать, если маркеры не отображаются
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
34SDS_IFRAME_PLACEHOLDER_0

plaintext

1