Geolocation API
The Geolocation API allows web applications to access the user's geographical location with their consent. It is part of the Navigator API and provides methods for one-time position requests, continuous position tracking, and manual position watching.
The API is available through navigator.geolocation and works over HTTPS only in modern browsers. It supports three methods: getCurrentPosition, watchPosition, and clearWatch.
Location data includes latitude, longitude, altitude, accuracy, heading, speed, and a timestamp. The browser determines the best source (GPS, Wi-Fi triangulation, IP geolocation) based on device capabilities and permissions.
The three core methods provide flexible access patterns for different use cases:
| Method | Description | Use Case |
|---|---|---|
| getCurrentPosition | One-time position request | Show user location on map, weather lookup |
| watchPosition | Continuous position updates | Turn-by-turn navigation, fitness tracking |
| clearWatch | Stop watching position | Cleanup when leaving a tracking view |
| 1 | // Get current position (one-time) |
| 2 | navigator.geolocation.getCurrentPosition( |
| 3 | (position) => { |
| 4 | const { latitude, longitude } = position.coords; |
| 5 | console.log("Latitude:", latitude); |
| 6 | console.log("Longitude:", longitude); |
| 7 | }, |
| 8 | (error) => { |
| 9 | console.error("Geolocation error:", error.message); |
| 10 | } |
| 11 | ); |
| 12 | |
| 13 | // Watch position (continuous tracking) |
| 14 | const watchId = navigator.geolocation.watchPosition( |
| 15 | (position) => { |
| 16 | updateMapMarker(position.coords.latitude, position.coords.longitude); |
| 17 | updateSpeed(position.coords.speed); |
| 18 | }, |
| 19 | (error) => handleLocationError(error), |
| 20 | { enableHighAccuracy: true, timeout: 5000, maximumAge: 0 } |
| 21 | ); |
| 22 | |
| 23 | // Stop watching when done |
| 24 | navigator.geolocation.clearWatch(watchId); |
info
The GeolocationPosition object contains a coords property with all location data and a timestamp property indicating when the position was acquired.
| Property | Type | Description |
|---|---|---|
| coords.latitude | number | Latitude in decimal degrees (WGS84) |
| coords.longitude | number | Longitude in decimal degrees (WGS84) |
| coords.accuracy | number | Accuracy radius in meters (lower = better) |
| coords.altitude | number | null | Altitude in meters above sea level |
| coords.heading | number | null | Direction of travel in degrees (0=N, 90=E) |
| coords.speed | number | null | Speed in meters per second |
| timestamp | number | DOMTimeStamp when position was acquired |
| 1 | navigator.geolocation.getCurrentPosition((pos) => { |
| 2 | const c = pos.coords; |
| 3 | |
| 4 | console.log("Position acquired at:", new Date(pos.timestamp).toISOString()); |
| 5 | console.log("Latitude:", c.latitude.toFixed(6)); |
| 6 | console.log("Longitude:", c.longitude.toFixed(6)); |
| 7 | console.log("Accuracy:", c.accuracy, "meters"); |
| 8 | |
| 9 | if (c.altitude !== null) { |
| 10 | console.log("Altitude:", c.altitude.toFixed(1), "meters"); |
| 11 | } |
| 12 | |
| 13 | if (c.heading !== null) { |
| 14 | console.log("Heading:", c.heading.toFixed(1), "degrees"); |
| 15 | } |
| 16 | |
| 17 | if (c.speed !== null) { |
| 18 | console.log("Speed:", (c.speed * 3.6).toFixed(1), "km/h"); |
| 19 | } |
| 20 | }); |
best practice
The error callback receives a GeolocationPositionError object with numeric codes and a human-readable message. Always handle all three error types gracefully.
| Constant | Code | Description |
|---|---|---|
| PERMISSION_DENIED | 1 | User denied the location permission |
| POSITION_UNAVAILABLE | 2 | Location provider returned no data |
| TIMEOUT | 3 | Position request exceeded the timeout |
| 1 | function handleLocationError(error) { |
| 2 | switch (error.code) { |
| 3 | case error.PERMISSION_DENIED: |
| 4 | showUI("Location access denied. Please enable in browser settings."); |
| 5 | console.warn("User denied geolocation permission"); |
| 6 | break; |
| 7 | |
| 8 | case error.POSITION_UNAVAILABLE: |
| 9 | showUI("Unable to determine location. Check GPS or network."); |
| 10 | console.warn("Position unavailable — GPS/Wi-Fi failure"); |
| 11 | break; |
| 12 | |
| 13 | case error.TIMEOUT: |
| 14 | showUI("Location request timed out. Try again."); |
| 15 | console.warn("Geolocation timeout exceeded"); |
| 16 | break; |
| 17 | |
| 18 | default: |
| 19 | showUI("An unknown error occurred: " + error.message); |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | function showUI(message) { |
| 24 | document.getElementById("location-status").textContent = message; |
| 25 | } |
| 26 | |
| 27 | navigator.geolocation.getCurrentPosition( |
| 28 | onSuccess, |
| 29 | handleLocationError, |
| 30 | { timeout: 10000 } |
| 31 | ); |
warning
The third parameter to both getCurrentPosition and watchPosition is an options object that controls accuracy, timeout, and caching behavior.
| Option | Default | Description |
|---|---|---|
| enableHighAccuracy | false | Use GPS for best accuracy (more battery/power) |
| timeout | Infinity | Max time (ms) to wait for a position |
| maximumAge | 0 | Accept cached position up to this age (ms) |
| 1 | // High accuracy for navigation (GPS required) |
| 2 | const navOptions = { |
| 3 | enableHighAccuracy: true, |
| 4 | timeout: 15000, |
| 5 | maximumAge: 0, // no cached positions |
| 6 | }; |
| 7 | |
| 8 | // Battery-friendly for weather widget |
| 9 | const weatherOptions = { |
| 10 | enableHighAccuracy: false, |
| 11 | timeout: 10000, |
| 12 | maximumAge: 300000, // accept 5-minute-old positions |
| 13 | }; |
| 14 | |
| 15 | // Fast approximate position |
| 16 | const quickOptions = { |
| 17 | enableHighAccuracy: false, |
| 18 | timeout: 5000, |
| 19 | maximumAge: 600000, // accept 10-minute-old positions |
| 20 | }; |
| 21 | |
| 22 | navigator.geolocation.getCurrentPosition( |
| 23 | success, error, navOptions |
| 24 | ); |
best practice
The Permissions API allows you to check geolocation permission status without triggering a prompt. This is useful for adapting the UI before requesting a position.
| 1 | // Check geolocation permission state |
| 2 | async function checkGeolocationPermission() { |
| 3 | const result = await navigator.permissions.query({ |
| 4 | name: "geolocation" |
| 5 | }); |
| 6 | |
| 7 | console.log("Permission state:", result.state); |
| 8 | // "granted" | "denied" | "prompt" |
| 9 | |
| 10 | result.addEventListener("change", () => { |
| 11 | console.log("Permission changed to:", result.state); |
| 12 | if (result.state === "granted") { |
| 13 | loadLocationFeatures(); |
| 14 | } |
| 15 | }); |
| 16 | |
| 17 | return result.state; |
| 18 | } |
| 19 | |
| 20 | // Usage |
| 21 | const permissionState = await checkGeolocationPermission(); |
| 22 | |
| 23 | switch (permissionState) { |
| 24 | case "granted": |
| 25 | // User already approved — call getCurrentPosition directly |
| 26 | loadMapData(); |
| 27 | break; |
| 28 | |
| 29 | case "prompt": |
| 30 | // Will show permission dialog — explain why location is needed first |
| 31 | showPermissionExplanation(); |
| 32 | break; |
| 33 | |
| 34 | case "denied": |
| 35 | // User previously denied — show instructions to re-enable |
| 36 | showLocationDisabledMessage(); |
| 37 | break; |
| 38 | } |
pro tip
The Geolocation API powers a wide range of location-aware features across many application types:
| Use Case | Accuracy Needed | Pattern |
|---|---|---|
| Maps & Navigation | High (5-50m) | watchPosition + enableHighAccuracy |
| Weather | Low (city-level) | getCurrentPosition + maximumAge |
| Nearby Search | Medium (100m-1km) | getCurrentPosition then search API |
| Fitness Tracking | High (GPS) | watchPosition + heading/speed |
| Geofencing | Medium | watchPosition + distance calculation |
| Content Personalization | Low (IP-level) | getCurrentPosition without highAccuracy |
| 1 | // Weather app — get city-level position |
| 2 | async function getWeather() { |
| 3 | const pos = await new Promise((resolve, reject) => { |
| 4 | navigator.geolocation.getCurrentPosition(resolve, reject, { |
| 5 | enableHighAccuracy: false, |
| 6 | timeout: 10000, |
| 7 | maximumAge: 600000 |
| 8 | }); |
| 9 | }); |
| 10 | |
| 11 | const { latitude, longitude } = pos.coords; |
| 12 | const response = await fetch( |
| 13 | `https://api.weather.example/current?${latitude},${longitude}` |
| 14 | ); |
| 15 | const weather = await response.json(); |
| 16 | showWeatherUI(weather); |
| 17 | } |
| 18 | |
| 19 | // Nearby places search |
| 20 | async function searchNearby() { |
| 21 | const pos = await getCurrentPosition(); |
| 22 | const { latitude, longitude } = pos.coords; |
| 23 | |
| 24 | const places = await fetch( |
| 25 | `/api/places/nearby?lat=${latitude}&lng=${longitude}&radius=1000` |
| 26 | ); |
| 27 | renderResults(places); |
| 28 | } |
| 29 | |
| 30 | // Distance between two coordinates (Haversine formula) |
| 31 | function haversineDistance(lat1, lon1, lat2, lon2) { |
| 32 | const R = 6371000; // Earth radius in meters |
| 33 | const toRad = (deg) => (deg * Math.PI) / 180; |
| 34 | |
| 35 | const dLat = toRad(lat2 - lat1); |
| 36 | const dLon = toRad(lon2 - lon1); |
| 37 | |
| 38 | const a = |
| 39 | Math.sin(dLat / 2) ** 2 + |
| 40 | Math.cos(toRad(lat1)) * |
| 41 | Math.cos(toRad(lat2)) * |
| 42 | Math.sin(dLon / 2) ** 2; |
| 43 | |
| 44 | return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); |
| 45 | } |
Live preview — simulated geolocation UI demonstrating the permission flow and coordinate display:
Following these guidelines ensures a respectful, performant, and user-friendly geolocation experience:
best practice
The browser permission prompt is the gatekeeper for geolocation access. A well-designed permission UX significantly increases acceptance rates and reduces user friction.
| 1 | // Step 1: Check permission without prompting |
| 2 | async function requestLocationFeature() { |
| 3 | const perm = await navigator.permissions.query({ name: "geolocation" }); |
| 4 | |
| 5 | if (perm.state === "granted") { |
| 6 | // Permission already given — proceed immediately |
| 7 | return getCurrentPosition(); |
| 8 | } |
| 9 | |
| 10 | if (perm.state === "denied") { |
| 11 | // Previously denied — show guidance, don't re-prompt |
| 12 | showLocationDisabledBanner(); |
| 13 | return null; |
| 14 | } |
| 15 | |
| 16 | // State is "prompt" — show explanation before requesting |
| 17 | showLocationExplanationDialog(); |
| 18 | } |
| 19 | |
| 20 | // Step 2: Show explanation dialog |
| 21 | function showLocationExplanationDialog() { |
| 22 | const dialog = document.getElementById("location-prompt"); |
| 23 | dialog.showModal(); |
| 24 | // Dialog contains a "Allow Location" button that calls: |
| 25 | // navigator.geolocation.getCurrentPosition(... |
| 26 | } |
| 27 | |
| 28 | // Step 3: Handle denial gracefully |
| 29 | function onLocationDenied() { |
| 30 | document.getElementById("location-status").innerHTML = ` |
| 31 | <div class="denied-banner"> |
| 32 | <span>\26A0 Location access denied</span> |
| 33 | <p>Enable location in your browser settings to use nearby features.</p> |
| 34 | <button onclick="openBrowserSettings()">Open Settings</button> |
| 35 | </div> |
| 36 | `; |
| 37 | } |
| 38 | |
| 39 | // Step 4: Handle re-enabling |
| 40 | navigator.permissions.query({ name: "geolocation" }).then((perm) => { |
| 41 | perm.addEventListener("change", () => { |
| 42 | if (perm.state === "granted") { |
| 43 | reloadLocationFeatures(); |
| 44 | } |
| 45 | }); |
| 46 | }); |
pro tip
Live preview — permission explanation dialog pattern: