Интерактивные карты, построенные на Mapbox GL JS, требуют особого внимания к доступности, поскольку большая часть функциональности представлена графически и не имеет семантического HTML-эквивалента. ARIA-атрибуты (Accessible Rich Internet Applications) компенсируют этот разрыв, позволяя экранным читалкам и вспомогательным технологиям интерпретировать карту и элементы управления корректно.
Карта в DOM обычно представляется как <canvas> или
набор WebGL-слоёв внутри контейнера <div>. Без
дополнительной разметки этот контейнер является «немым» элементом для
вспомогательных технологий, поэтому ключевая задача — задать ему
корректную семантику.
Основной контейнер карты должен быть явно обозначен как интерактивный регион. На практике применяются следующие подходы:
role="application" — указывает, что внутри находится
сложное интерактивное приложениеaria-label — описывает назначение картыtabindex="0" — добавляет возможность фокусировкиПример конфигурации:
<div id="map"
role="application"
aria-label="Интерактивная карта"
tabindex="0"></div>
Использование role="application" требует осторожности:
вспомогательные технологии начинают передавать управление клавиатурой
внутрь приложения, поэтому важно обеспечить полноценную навигацию внутри
карты.
Состояние карты динамично: масштаб, координаты, наклон, вращение. Эти изменения должны быть доступны для экранных читалок.
Часто используется связка:
aria-live="polite" — для ненавязчивых обновленийaria-atomic="true" — для полного обновления текста
состоянияПример элемента состояния:
<div id="map-status"
aria-live="polite"
aria-atomic="true"></div>
При изменении карты (pan, zoom, rotate) обновляется текст:
map.on('moveend', () => {
const center = map.getCenter();
const zoom = map.getZoom();
document.getElementById('map-status').textContent =
`Центр: ${center.lat.toFixed(4)}, ${center.lng.toFixed(4)}. Масштаб: ${zoom.toFixed(2)}`;
});
Элементы управления (zoom, rotation, geolocate) в Mapbox GL JS создаются программно. Для них критично наличие ARIA-меток.
map.addControl(new mapboxgl.NavigationControl({
showCompass: true,
showZoom: true
}));
После добавления DOM-элементы получают стандартные кнопки без достаточной семантики. Поэтому требуется уточнение:
aria-label="Приблизить"aria-label="Отдалить"aria-label="Сбросить поворот"В кастомных реализациях:
<button aria-label="Приблизить карту">+</button>
<button aria-label="Отдалить карту">−</button>
Mapbox GL JS поддерживает клавиатурное управление, но его поведение зависит от конфигурации контейнера.
Ключевые аспекты:
tabindex для входа в карту+ / - для масштабаShift + стрелки для более грубого панорамированияПри создании кастомных обработчиков важно сохранять ожидаемую модель поведения:
map.getCanvas().setAttribute('tabindex', '0');
map.on('keydown', (e) => {
if (e.originalEvent.key === '+') {
map.zoomIn();
}
if (e.originalEvent.key === '-') {
map.zoomOut();
}
});
Маркерные элементы часто реализуются как HTML-элементы поверх canvas. Они должны быть доступны:
role="button" для интерактивных маркеровaria-label с названием точкиtabindex="0" для фокусировкиПример:
<div class="marker"
role="button"
tabindex="0"
aria-label="Офис компании"></div>
При создании через API:
const el = document.createElement('div');
el.setAttribute('role', 'button');
el.setAttribute('aria-label', 'Точка интереса');
el.tabIndex = 0;
Карта постоянно меняет контекст: пользователь перемещается между регионами, слоями, объектами. Для передачи этих изменений используются ARIA-live регионы.
Типичные события:
Пример обработки выбора объекта:
map.on('click', 'points-layer', (e) => {
const feature = e.features[0];
document.getElementById('map-status').textContent =
`Выбран объект: ${feature.properties.name}`;
});
Popup-окна часто реализуются как отдельные DOM-элементы. Их необходимо снабжать:
role="dialog"aria-labelledbyaria-describedbyПример:
const popup = new mapboxgl.Popup({
closeButton: true,
closeOnClick: true
})
.setHTML(`
<div role="dialog" aria-labelledby="popup-title">
<h3 id="popup-title">Объект</h3>
<p>Описание точки интереса</p>
</div>
`);
Кнопка закрытия должна иметь:
aria-label="Закрыть"Хотя ARIA не отвечает за визуальные параметры, доступность тесно связана с:
:focus-visible)Пример CSS:
.mapboxgl-ctrl button:focus-visible {
outline: 3px solid #005fcc;
}
При асинхронной загрузке карты и тайлов важно отражать состояние:
aria-busy="true" при загрузкеaria-disabled="true" для временно неактивных
контроловmap.on('loading', () => {
map.getContainer().setAttribute('aria-busy', 'true');
});
map.on('idle', () => {
map.getContainer().setAttribute('aria-busy', 'false');
});
Слои карты часто представлены визуально, но требуют текстового эквивалента:
aria-controls между переключателем и
содержимымПример:
<button aria-controls="layer-roads" aria-pressed="true">
Дороги
</button>
<div id="layer-roads">
Отображение дорожной сети
</div>
События карты позволяют синхронизировать ARIA-состояния:
movezoomrotateloadclickПример универсального обновления статуса:
function updateStatus(text) {
document.getElementById('map-status').textContent = text;
}
map.on('zoomend', () => {
updateStatus(`Новый масштаб: ${map.getZoom().toFixed(2)}`);
});
map.on('rotateend', () => {
updateStatus(`Поворот: ${map.getBearing().toFixed(1)}°`);
});
Типичные проблемы:
role="application" без
клавиатурной поддержкиaria-label у интерактивных элементовaria-live) с высокой
частотой обновленийПравильный подход — описывать только значимые изменения состояния, а не визуальную детализацию.
Хорошо организованная карта строится слоями:
Такая структура обеспечивает предсказуемое взаимодействие вспомогательных технологий с динамическим WebGL-интерфейсом, где визуальная часть не может быть интерпретирована напрямую.