«Найти ближайший пункт выдачи», «показать погоду там, где я сейчас», «подставить город в форму доставки» — за всеми такими кнопками стоит один и тот же браузерный интерфейс, Geolocation API. Он спрашивает у устройства, где оно находится, и возвращает широту, долготу и оценку точности. Никаких библиотек и ключей для этого не нужно, достаточно объекта navigator.geolocation.
Ниже разберём весь путь: откуда браузер вообще берёт координаты, как сделать первый запрос и обработать отказ, как узнать о разрешении заранее, как нарисовать точку на карте и что поменял новый HTML-элемент <geolocation>.
Откуда браузер знает, где пользователь
Координаты не берутся из одного источника. Браузер (точнее, операционная система под ним) собирает их из того, что есть под рукой:
- GPS (спутниковая навигация) — самый точный вариант, до нескольких метров, но есть в основном на телефонах и плохо работает в помещении;
- Wi-Fi — по списку видимых точек доступа и базе их расположения; в городе даёт десятки метров;
- вышки сотовой связи — сотни метров или километры;
- IP-адрес — самый грубый способ, иногда промахивается на целый город.
Отсюда главное практическое следствие: у каждого ответа есть поле accuracy — радиус погрешности в метрах. Ноутбук без GPS спокойно вернёт точку с погрешностью в 2–3 километра, и это не ошибка, а честная оценка. Код, который ставит маркер «вы здесь» и молча игнорирует погрешность, будет врать пользователю.
Второе ограничение — API работает только в безопасном контексте: на страницах по HTTPS (защищённому протоколу) и на localhost для разработки. На обычном HTTP запрос сразу заканчивается ошибкой. И третье — без согласия пользователя координат не будет никогда: браузер обязательно показывает запрос разрешения, а отказ — такой же штатный сценарий, как и успех.

Первый запрос: getCurrentPosition
Основной метод — getCurrentPosition(). Он ничего не возвращает: результат приходит в колбэк успеха, проблема — в колбэк ошибки. Минимальная версия с кнопкой и блоком для вывода:
<button type="button" id="locate-btn">Где я?</button>
<p id="locate-status"></p>
const statusEl = document.querySelector('#locate-status');
function onSuccess(position) {
const { latitude, longitude, accuracy } = position.coords;
statusEl.textContent =
`Широта ${latitude.toFixed(5)}, долгота ${longitude.toFixed(5)} (±${Math.round(accuracy)} м)`;
}
function onError(error) {
statusEl.textContent = describeGeoError(error);
}
document.querySelector('#locate-btn').addEventListener('click', () => {
if (!('geolocation' in navigator)) {
statusEl.textContent = 'Браузер не умеет определять местоположение';
return;
}
statusEl.textContent = 'Определяем местоположение…';
navigator.geolocation.getCurrentPosition(onSuccess, onError);
});
Строка «Определяем местоположение» тут не для красоты: ответ приходит не мгновенно — сначала пользователь думает над запросом разрешения, потом устройство ищет спутники или Wi-Fi. Без промежуточного статуса кнопка выглядит сломанной.
Что лежит в ответе
В колбэк успеха приходит объект GeolocationPosition с двумя полями: timestamp (момент измерения) и coords. Внутри coords:
- latitude и longitude — широта и долгота в градусах;
- accuracy — погрешность в метрах, есть всегда;
- altitude, altitudeAccuracy — высота и её погрешность;
- heading — направление движения в градусах от севера;
- speed — скорость в метрах в секунду.
Последние пять полей устройство заполняет, только если умеет их измерять. На ноутбуке они почти всегда равны null, так что код должен быть к этому готов.
Долгое время у этих объектов была неприятная особенность: JSON.stringify(position) возвращал пустой объект {}, потому что все поля живут на прототипе, а не на самом объекте. Приходилось перекладывать координаты в обычный объект вручную. Теперь у GeolocationPosition и GeolocationCoordinates есть метод toJSON(), и позицию можно сразу отправить на сервер:
navigator.geolocation.getCurrentPosition((position) => {
fetch('/api/nearest-store', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(position),
// {"timestamp":1767300000000,"coords":{"latitude":50.45,"longitude":30.52,"accuracy":35,...}}
});
});
В старых браузерах такой код молча отправит {"timestamp":…,"coords":{}} или вовсе пустой объект, поэтому, если аудитория широкая, надёжнее собрать нужные поля вручную.
Три вида ошибок
В колбэк ошибки приходит GeolocationPositionError с числовым полем code и техническим message. Кодов по спецификации ровно три, и у каждого есть именованная константа — сравнивать с ней читабельнее, чем с голыми единицей, двойкой и тройкой:
function describeGeoError(error) {
switch (error.code) {
case error.PERMISSION_DENIED: // 1
return 'Доступ к местоположению запрещён. Его можно включить в настройках сайта.';
case error.POSITION_UNAVAILABLE: // 2
return 'Устройство не смогло определить координаты. Попробуйте позже или у окна.';
case error.TIMEOUT: // 3
return 'Не дождались ответа от устройства.';
default:
return 'Что-то пошло не так: ' + error.message;
}
}
Ветка default — на всякий случай: новых кодов в спецификации нет, но защититься от неожиданного ничего не стоит. Поле message пользователю лучше не показывать — там отладочный текст на английском.
Опции: точность, таймаут и кэш
Третий аргумент getCurrentPosition() — объект настроек. Про него часто забывают, а зря: значения по умолчанию подходят далеко не всем сценариям.
navigator.geolocation.getCurrentPosition(onSuccess, onError, {
enableHighAccuracy: true, // по умолчанию false
timeout: 10_000, // по умолчанию Infinity
maximumAge: 60_000, // по умолчанию 0
});
- enableHighAccuracy — просьба дать самые точные координаты. На телефоне это обычно значит включить GPS: ответ медленнее, батарея тратится быстрее. Для «в каком я городе» не нужно, для навигации — нужно.
- timeout — сколько миллисекунд ждать ответа. По умолчанию Infinity, то есть ждать вечно: если устройство не может поймать сигнал, колбэк не вызовется никогда, и интерфейс зависнет на «Определяем…». Ставить таймаут стоит почти всегда. Важно, что отсчёт начинается после того, как пользователь дал разрешение, а не с момента вызова.
- maximumAge — насколько старые закэшированные координаты согласны принять. При 0 браузер каждый раз меряет заново, при 60_000 отдаст позицию минутной давности мгновенно, если она есть.
С колбэками неудобно работать в async-коде, поэтому метод часто оборачивают в промис. Обёртка короткая и переиспользуется во всём проекте:
function getPosition(options = {}) {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
timeout: 10_000,
...options,
});
});
}
async function fillCityField() {
try {
const { coords } = await getPosition({ maximumAge: 300_000 });
cityInput.value = await lookupCity(coords.latitude, coords.longitude);
} catch (error) {
// пользователь отказал или не дождались — оставляем поле пустым
}
}
Сначала проверить, потом спрашивать
Самая частая ошибка с геолокацией — вызвать getCurrentPosition() прямо при загрузке страницы. Пользователь ещё не понял, что это за сайт, а у него уже просят местоположение; рефлекторный ответ — «Блокировать». И это решение надолго: после отказа сайт больше не может показать запрос, каждый следующий вызов сразу падает в PERMISSION_DENIED, а вернуть доступ пользователь может только сам, через настройки сайта.
Правильный порядок такой: сначала узнать текущее состояние разрешения, а запрос показывать по клику, объяснив, зачем нужны координаты. Состояние отдаёт Permissions API:
const permission = await navigator.permissions.query({ name: 'geolocation' });
function render(state) {
if (state === 'granted') {
// разрешение уже есть — можно определять сразу, без кнопки
locateAndShow();
} else if (state === 'prompt') {
// ещё не спрашивали — показываем кнопку с пояснением
showLocateButton('Покажем ближайшие пункты выдачи');
} else {
// 'denied' — запрос не появится, подсказываем, где включить
showManualCityInput();
}
}
render(permission.state);
// пользователь может поменять решение в настройках, не перезагружая страницу
permission.addEventListener('change', () => render(permission.state));
Три состояния — granted (разрешено), prompt (ещё не спрашивали) и denied (запрещено). Сам по себе query() никакого запроса пользователю не показывает, это просто справка. А на ветку denied всегда стоит иметь запасной путь — ручной выбор города, поле для адреса — иначе функция сайта для этого пользователя просто перестаёт существовать.
Слежение за перемещением: watchPosition
Если координаты нужны не один раз, а постоянно — трекер пробежки, курьер на карте, «до остановки 300 метров», — вместо повторных вызовов getCurrentPosition() используют watchPosition(). Сигнатура та же, но колбэк успеха вызывается каждый раз, когда позиция меняется. Метод возвращает числовой идентификатор, по которому слежение потом выключают:
let watchId = null;
startBtn.addEventListener('click', () => {
watchId = navigator.geolocation.watchPosition(
({ coords }) => updateRunnerMarker(coords.latitude, coords.longitude),
onError,
{ enableHighAccuracy: true, maximumAge: 5_000 }
);
});
stopBtn.addEventListener('click', () => {
navigator.geolocation.clearWatch(watchId);
watchId = null;
});
Слежение с enableHighAccuracy держит GPS включённым и заметно садит батарею, поэтому clearWatch() нужно вызывать, как только данные перестали быть нужны: пользователь ушёл с экрана карты, закончил тренировку, закрыл модалку.
Точка на карте: Leaflet и OpenStreetMap
Пара чисел с пятью знаками после запятой мало что говорит человеку — координаты хочется увидеть на карте. Самый лёгкий путь без ключей и регистрации — библиотека Leaflet плюс тайлы (квадратные фрагменты карты) из OpenStreetMap, открытой карты, которую наполняет сообщество.
Подключаем стили и скрипт стабильной версии и готовим контейнер:
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<div id="user-map"></div>
#user-map {
height: 360px; /* без явной высоты карта схлопнется в 0 */
}
Комментарий в CSS — не формальность. Leaflet растягивает карту на размер контейнера, а у пустого <div> высота нулевая. Это самая частая причина вопроса «почему карта не показывается». Родственная ловушка: если контейнер был скрыт через display: none в момент создания карты, после показа нужно вызвать map.invalidateSize(), иначе тайлы лягут только в угол.
Дальше — создаём карту в колбэке успеха, ставим маркер и рисуем круг погрешности:
function showOnMap({ coords }) {
const point = [coords.latitude, coords.longitude];
const map = L.map('user-map').setView(point, 15);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(map);
L.marker(point).addTo(map).bindPopup('Вы примерно здесь');
const accuracyCircle = L.circle(point, {
radius: coords.accuracy, // в метрах, прямо из ответа API
weight: 1,
fillOpacity: 0.15,
}).addTo(map);
// если погрешность большая, отдаляем карту, чтобы круг поместился целиком
map.fitBounds(accuracyCircle.getBounds());
}
navigator.geolocation.getCurrentPosition(showOnMap, onError, { timeout: 10_000 });
Круг погрешности — то, чего не хватает большинству туториалов. На телефоне с GPS он почти совпадает с маркером, на ноутбуке может накрыть полгорода, и пользователь сразу видит, насколько точке можно верить. Именно так ведут себя карты в навигаторах.
Правила пользования тайлами
Тайловый сервер OpenStreetMap бесплатный, но не безусловный. Из правил пользования, которые чаще всего нарушают в учебных примерах:
- Адрес — только https://tile.openstreetmap.org/ без поддоменов. Старый шаблон с {s} (поддомены a, b, c) ещё кочует по статьям, но эти адреса могут отключить без предупреждения.
- Атрибуция — подпись «© OpenStreetMap» должна быть видна прямо на карте. Прятать её стилями или за переключателем нельзя.
- Referer (заголовок, по которому сервер видит, с какого сайта пришёл запрос) — должен отправляться. Слишком строгая Referrer-Policy на сайте приводит к блокировке.
- Нагрузка — массово скачивать тайлы, качать их заранее для офлайна и строить на этом сервере высоконагруженный продукт нельзя. Для продакшена с большим трафиком берут коммерческого поставщика тайлов или поднимают свой сервер.
Пара слов про версии. Примеры выше написаны для Leaflet 1.9.4 — это актуальная стабильная ветка. Параллельно идёт работа над Leaflet 2.0: он пока в статусе alpha, переходит на ES-модули и классы вместо фабричных функций (вместо L.map() и L.marker() — импортируемые LeafletMap и Marker). Переписывать код на него в продакшене стоит после стабильного релиза.
Элемент <geolocation>: запрос без JavaScript
Связка «кнопка → обработчик → getCurrentPosition() → два колбэка» давно стала шаблоном, и у неё есть общая проблема: браузер не может отличить осмысленный клик пользователя от скрипта, который дёрнул запрос сам. Отсюда и вал запросов при загрузке, и привычка пользователей не глядя нажимать «Блокировать».
Chrome 144 добавил декларативную альтернативу — HTML-элемент <geolocation>. Это кнопка, которую рисует сам браузер: текст и иконку задаёт он, а стили страницы могут поменять цвета и размеры, но не сделать кнопку невидимой, полупрозрачной или неконтрастной. Поэтому клик по ней — гарантированно осознанное действие, и браузер доверяет ему больше, чем обычной кнопке.
<geolocation id="geo" accuracymode="precise">
<!-- фолбэк: его увидят браузеры, которые элемент не знают -->
<button type="button" id="geo-fallback">Показать, где я</button>
</geolocation>
const geoEl = document.querySelector('#geo');
geoEl.addEventListener('location', () => {
if (geoEl.position) {
showOnMap(geoEl.position); // тот же GeolocationPosition
} else if (geoEl.error) {
statusEl.textContent = describeGeoError(geoEl.error);
}
});
// для браузеров без элемента внутренняя кнопка работает по-старому
document.querySelector('#geo-fallback').addEventListener('click', () => {
navigator.geolocation.getCurrentPosition(showOnMap, onError, { timeout: 10_000 });
});
Что умеет элемент:
- атрибут accuracymode — approximate (по умолчанию) или precise, аналог enableHighAccuracy;
- атрибут autolocate — если разрешение уже выдано, координаты запрашиваются сразу при загрузке, без клика;
- атрибут watch — превращает разовый запрос в слежение, как watchPosition();
- событие location и свойства position / error — те же объекты, что и в классическом API, так что функции разбора ответа переиспользуются без изменений;
- CSS-псевдокласс :granted — срабатывает, когда разрешение выдано, и позволяет перекрасить кнопку без JavaScript.
geolocation {
background-color: #1d4ed8;
color: #fff;
border-radius: 8px;
}
geolocation:granted {
background-color: #15803d;
}
Фолбэк работает за счёт старого правила HTML: незнакомый тег браузер считает обычным строчным элементом и просто показывает его содержимое. Firefox и Safari увидят вложенную кнопку и пойдут по классическому пути, Chrome и Edge нарисуют свою. Проверить поддержку из скрипта можно через 'HTMLGeolocationElement' in window.
Элемент вырос из экспериментального <permission>, который Chrome обкатывал в пробном режиме. От универсального варианта отказались в пользу отдельных элементов под каждую возможность — по тому же принципу сейчас тестируется элемент для камеры и микрофона. Стандартизация ещё идёт, поэтому <geolocation> — это прогрессивное улучшение поверх классического API, а не замена ему.
Частые ошибки
- Запрос при загрузке страницы. Пользователь отказывает, не разобравшись, и второго шанса у сайта нет. Спрашиваем по клику и с объяснением, зачем.
- Нет таймаута. По умолчанию браузер ждёт бесконечно, и интерфейс навсегда остаётся в состоянии загрузки.
- Нет запасного пути. Отказ, отсутствие сигнала и старый браузер — нормальные сценарии. Ручной ввод города или адреса должен работать всегда.
- Тестирование по HTTP. На стенде с обычным HTTP API не работает, и кажется, что сломан код. Для разработки подходит localhost, для стенда — только HTTPS.
- Карта внутри iframe. Встроенная во фрейм страница по умолчанию не имеет доступа к геолокации, даже на HTTPS. Родительская страница должна явно его разрешить: <iframe src="…" allow="geolocation">.
- Слепая вера в точку. Без учёта accuracy ноутбук с погрешностью в километры получит «ближайший магазин» в соседнем районе.
Ещё одно соображение, не техническое: координаты — персональные данные. Если для задачи хватает города, не стоит хранить на сервере точную точку, а если координаты всё же уходят на бэкенд, об этом стоит честно написать рядом с кнопкой.
Итог
Geolocation API — один из самых старых и простых браузерных интерфейсов, но вокруг него много граблей. Рабочий минимум выглядит так: проверить разрешение через navigator.permissions, спросить координаты по клику с понятным пояснением, задать timeout, обработать все три кода ошибки и всегда учитывать погрешность. Там, где поддерживается элемент <geolocation>, браузер берёт часть этой работы на себя, а вложенная кнопка-фолбэк сохраняет классический путь для остальных.
И карта — далеко не единственное применение. По координатам можно подставить город в форму, отсортировать список магазинов по расстоянию, выбрать язык и валюту по умолчанию или показать местную погоду.
Комментарии (0)