DivIcon для HTML-маркеров

L.DivIcon представляет собой расширение стандартной иконки маркера, позволяющее использовать произвольную HTML-разметку вместо изображения. В отличие от L.Icon, который ограничен картинками и спрайтами, DivIcon рендерится как DOM-элемент, что открывает доступ к полноценной стилизации через CSS, динамическому контенту и интерактивным элементам.

Основная особенность заключается в том, что маркер становится обычным div-элементом внутри карты, сохраняя при этом все механизмы позиционирования Leaflet.


Создание базового HTML-маркера

Минимальная конфигурация DivIcon строится вокруг HTML-строки:

const divIcon = L.divIcon({
  html: '<div class="custom-marker"></div>',
  className: 'custom-icon',
  iconSize: [30, 30]
});

L.marker([55.75, 37.61], { icon: divIcon }).addTo(map);

В этом примере:

  • html задаёт внутреннюю разметку маркера
  • className добавляет класс к обёртке Leaflet
  • iconSize определяет размеры области, занимаемой иконкой

Leaflet вставляет HTML внутрь контейнера, который затем позиционируется на карте как обычный маркер.


Параметр html: структура и динамическое содержимое

html может содержать любую допустимую HTML-разметку:

L.divIcon({
  html: `
    <div class="pin">
      <span class="pin__circle"></span>
      <span class="pin__pulse"></span>
    </div>
  `,
  className: ''
});

Использование пустого className часто применяется для отключения стандартных стилей Leaflet (leaflet-div-icon), чтобы полностью контролировать внешний вид.

HTML может формироваться динамически:

function createUserMarker(user) {
  return L.divIcon({
    html: `
      <div class="user-avatar">
        <img src="${user.avatar}" />
        <span>${user.name}</span>
      </div>
    `,
    className: 'user-marker'
  });
}

Ключевые параметры DivIcon

iconSize

Определяет размер контейнера:

iconSize: [40, 40]

Если не задан, Leaflet не сможет корректно вычислить смещение центра.


iconAnchor

Определяет точку привязки внутри иконки:

iconAnchor: [20, 40]

Для «пина» обычно используется нижний центр.


popupAnchor

Смещение всплывающего окна относительно маркера:

popupAnchor: [0, -40]

className

Позволяет добавлять CSS-классы к контейнеру:

className: 'city-marker highlight'

Если требуется полное отключение стандартной обёртки:

className: ''

Стилизация через CSS

HTML-маркеры полностью управляются стилями:

.custom-marker {
  width: 20px;
  height: 20px;
  background: #2a9d8f;
  border-radius: 50%;
  box-shadow: 0 0 6px rgba(0,0,0,0.3);
}

Для сложных маркеров используется многослойная структура:

.pin {
  position: relative;
  width: 30px;
  height: 30px;
}

.pin__circle {
  width: 14px;
  height: 14px;
  background: #e63946;
  border-radius: 50%;
  position: absolute;
  top: 8px;
  left: 8px;
}

.pin__pulse {
  position: absolute;
  width: 30px;
  height: 30px;
  background: rgba(230, 57, 70, 0.4);
  border-radius: 50%;
  animation: pulse 1.5s infinite;
}

Динамические HTML-маркеры

DivIcon позволяет обновлять содержимое без пересоздания маркера:

const marker = L.marker([55.75, 37.61], {
  icon: L.divIcon({
    html: '<div class="counter">0</div>',
    className: ''
  })
}).addTo(map);

function updateCount(value) {
  const el = marker.getElement();
  if (!el) return;

  el.querySelector('.counter').textContent = value;
}

Доступ к DOM осуществляется через getElement(), что позволяет напрямую менять структуру внутри маркера.


Использование с данными (data-driven markers)

DivIcon часто применяется для визуализации числовых и категориальных данных:

const stations = [
  { coords: [55.75, 37.61], load: 12 },
  { coords: [55.76, 37.62], load: 87 }
];

stations.forEach(s => {
  L.marker(s.coords, {
    icon: L.divIcon({
      html: `<div class="load ${s.load > 50 ? 'high' : 'low'}">${s.load}</div>`,
      className: ''
    })
  }).addTo(map);
});

Такой подход позволяет кодировать состояние прямо в интерфейсе маркера.


Интерактивность и события

HTML внутри DivIcon может содержать элементы, реагирующие на события:

const icon = L.divIcon({
  html: `
    <button class="marker-btn">OK</button>
  `,
  className: ''
});

const marker = L.marker([55.75, 37.61], { icon }).addTo(map);

marker.on('add', () => {
  const el = marker.getElement();
  el.querySelector('.marker-btn').addEventListener('click', () => {
    console.log('click on marker button');
  });
});

Leaflet не управляет внутренними событиями DOM, поэтому обработка выполняется вручную.


Совмещение с popup и tooltip

DivIcon полностью совместим с всплывающими окнами:

marker.bindPopup(`
  <div class="popup-content">
    <h3>Объект</h3>
    <p>Описание точки на карте</p>
  </div>
`);

Также поддерживаются tooltip:

marker.bindTooltip('Подсказка', {
  direction: 'top'
});

Производительность и ограничения

Использование HTML-маркеров увеличивает нагрузку на DOM:

  • каждый маркер — отдельный div
  • при большом количестве объектов возможны просадки производительности
  • анимации внутри маркеров усиливают нагрузку

При работе с тысячами точек часто используется:

  • кластеризация (Leaflet.markercluster)
  • упрощённые L.CircleMarker
  • виртуализация видимых объектов

Типовые шаблоны DivIcon

Числовой маркер

L.divIcon({
  html: '<div class="num-marker">5</div>',
  className: ''
});

Маркер с аватаром

L.divIcon({
  html: `
    <div class="avatar-marker">
      <img src="avatar.jpg">
    </div>
  `,
  className: ''
});

Статусный маркер

L.divIcon({
  html: `
    <div class="status ${status}">
      <span></span>
    </div>
  `,
  className: ''
});

Поведение при масштабировании карты

DivIcon масштабируется как обычный DOM-элемент, не завися от географического масштаба. Это создаёт важное отличие от canvas-слоёв:

  • размер маркера остаётся фиксированным в пикселях
  • визуальная плотность меняется при зуме
  • возможно перекрытие элементов при высоком зуме

Частые ошибки при использовании DivIcon

Неправильное смещение точки привязки приводит к смещению маркера относительно координаты:

iconAnchor: [0, 0] // приводит к "съезду" позиции

Отсутствие iconSize усложняет расчёт центрирования.

Использование сложного DOM без оптимизации вызывает падение производительности на мобильных устройствах.

Обработчики событий, навешанные до появления элемента в DOM, не срабатывают без привязки к marker.on('add').


Взаимодействие с кастомной логикой рендеринга

DivIcon часто используется как слой представления в более сложных архитектурах:

  • данные хранятся отдельно от DOM
  • HTML генерируется функцией от состояния
  • обновление происходит через перерисовку или прямое изменение innerHTML
function renderIcon(data) {
  return L.divIcon({
    html: `<div class="point ${data.type}">${data.value}</div>`,
    className: ''
  });
}

Такой подход позволяет интегрировать карту в SPA-архитектуры без привязки к Leaflet-рендерингу как к единственному источнику состояния.