To get the user's location in HTML, call navigator.geolocation.getCurrentPosition() from JavaScript, ideally from a button click. The browser asks the user for permission, then calls your function with the latitude, longitude and accuracy, or calls your error function with a code from 1 to 3.
Try the button below, then the sample button next to it.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Geolocation request</title>
<style>
body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
.card { max-width: 460px; padding: 16px 18px; border-radius: 12px; background: #fff; box-shadow: 0 6px 20px rgba(0, 0, 0, .1); }
.card p { margin: 0 0 12px; font-size: 14px; color: #4b5563; }
.row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
button { font: inherit; font-size: 14px; padding: 9px 14px; border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; cursor: pointer; }
button.alt { background: #e5e7eb; color: #1d2330; }
label { font-size: 13px; color: #374151; }
.out { margin-top: 12px; font: 13px/1.55 ui-monospace, Consolas, monospace; padding: 8px 10px; border-radius: 8px; background: #eef1f5; overflow-wrap: anywhere; }
.out.ok { background: #d6f2df; color: #0f5132; }
.out.err { background: #fde2da; color: #9a3412; }
.codes { margin-top: 10px; font-size: 13px; border-collapse: collapse; width: 100%; }
.codes td { padding: 4px 6px; border-top: 1px solid #e5e7eb; vertical-align: top; }
.codes tr.hit { background: #fde2da; }
</style>
</head>
<body>
<div class="card">
<p>Where are you? The answer is only used on this page to show the numbers below.</p>
<div class="row">
<button id="ask" type="button">Get my location</button>
<button id="sample" type="button" class="alt">Use sample coordinates</button>
</div>
<div class="row" style="margin-top:8px">
<label><input type="checkbox" id="high"> enableHighAccuracy</label>
<label>timeout <input type="number" id="timeout" value="10000" min="0" step="1000" style="width:80px"> ms</label>
</div>
<div class="out" id="out">secure context: <span id="sec"></span>. Press a button.</div>
<table class="codes">
<tr id="c1"><td><b>1</b></td><td>PERMISSION_DENIED: refused now or earlier, not a secure page, or a frame without the geolocation permission</td></tr>
<tr id="c2"><td><b>2</b></td><td>POSITION_UNAVAILABLE: the device could not work out a position</td></tr>
<tr id="c3"><td><b>3</b></td><td>TIMEOUT: no position within the timeout</td></tr>
</table>
</div>
<script>
const out = document.getElementById('out');
document.getElementById('sec').textContent = window.isSecureContext;
function show(lat, lon, acc, source) {
out.className = 'out ok';
out.textContent = source + ': ' + lat.toFixed(5) + ', ' + lon.toFixed(5) +
' (accuracy ' + Math.round(acc) + ' m)';
}
document.getElementById('ask').addEventListener('click', () => {
document.querySelectorAll('.codes tr').forEach((tr) => tr.classList.remove('hit'));
if (!('geolocation' in navigator)) {
out.className = 'out err';
out.textContent = 'This browser has no geolocation API.';
return;
}
out.className = 'out';
out.textContent = 'Waiting for the browser...';
navigator.geolocation.getCurrentPosition(
(pos) => show(pos.coords.latitude, pos.coords.longitude, pos.coords.accuracy, 'You'),
(err) => {
// err.code is 1, 2 or 3; err.message is the browser's own wording
out.className = 'out err';
out.textContent = 'Error ' + err.code + ': ' + err.message;
document.getElementById('c' + err.code).classList.add('hit');
},
{
enableHighAccuracy: document.getElementById('high').checked,
timeout: Number(document.getElementById('timeout').value),
maximumAge: 0 // do not reuse an old position
}
);
});
// a fixed point so the rest of the page can be tried without permission
document.getElementById('sample').addEventListener('click', () => {
show(51.50080, -0.12460, 25, 'Sample (Westminster, London)');
});
</script>
</body>
</html>
Inside this article, the example runs in an embedded frame without the geolocation permission. In Chromium the request fails at once with error 1, "Geolocation has been disabled in this document by permissions policy", and no prompt appears.
That is why every example here has a sample-coordinates button. Opened as its own page over HTTPS, the same code shows the browser's prompt.
How the code works
getCurrentPosition takes up to three arguments: a function for success, a function for failure, and an options object. It returns nothing. The answer arrives later, in one of the two functions.
button.addEventListener('click', () => {
navigator.geolocation.getCurrentPosition(
(pos) => {
const { latitude, longitude, accuracy } = pos.coords;
show(latitude, longitude, accuracy); // accuracy is in metres
},
(err) => {
showError(err.code, err.message); // 1, 2 or 3
},
{ enableHighAccuracy: false, timeout: 10000, maximumAge: 0 }
);
});
Before a position arrives, the request passes four checks. Any of the first three can stop it with error 1.

The page must be a secure context: HTTPS, or localhost and 127.0.0.1 while you develop. What HTTPS is covers the rest. In Chromium, navigator.geolocation still exists on a plain http:// page, so check window.isSecureContext rather than only testing for the object.
Ask at the moment you need it
The first call on a site shows the browser's permission prompt. Whatever the user picks is usually remembered for that site. A call on page load, before the reader knows why, spends that one chance on a prompt with no context.

- Ask from a click. Put the call in the handler of a button such as "Use my location".
- Say why, next to the button. One line: what you use it for and whether it leaves the page.
- Keep a fallback. A postcode field, a city list or sample coordinates, so a refusal is not a dead end.
- Keep only what you need. Rounding to 3 decimal places keeps latitude to about 111 metres, which is enough to pick a nearby store.
Options and accuracy
The third argument tunes the request. All three options are optional.
| Option | Default | What it does |
|---|---|---|
enableHighAccuracy |
false |
Asks for the most precise position the device can give. It can be slower and use more power |
timeout |
Infinity |
Milliseconds to wait for a position before error 3. Time spent on the permission prompt does not count |
maximumAge |
0 |
Accepts a cached position up to this many milliseconds old. 0 means a fresh one every time |
The success callback gets a position object. coords.latitude and coords.longitude are in decimal degrees. coords.accuracy is a radius in metres around that point: the smaller it is, the more precise the position.
altitude, heading and speed are there too, but they can be null when the device does not know them. pos.timestamp says when the position was taken.
The three error codes
The error callback gets an object with code and message. The code is the reliable part. The message is the browser's own wording and differs between browsers.
| Code | Name | Usual causes | What to show |
|---|---|---|---|
| 1 | PERMISSION_DENIED |
The user refused, now or earlier. Or the page is in a frame without permission. In Chromium, also an insecure page | "Location is off for this site." Offer the fallback |
| 2 | POSITION_UNAVAILABLE |
The device could not work out a position | "Could not find your location." Offer a retry |
| 3 | TIMEOUT |
No position within timeout |
Retry with a longer timeout, or without high accuracy |
Compare against the named constants, such as err.code === err.PERMISSION_DENIED, or against the numbers. Both are the same values.
Distance without a map library
A position is just two numbers, so you do not need a map to use it. To compare it with other places, compute the straight-line distance with the haversine formula.
It treats the Earth as a sphere of radius 6,371 km, which is off by up to about 0.5%.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Distance between two points</title>
<style>
body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
.card { max-width: 460px; padding: 16px 18px; border-radius: 12px; background: #fff; box-shadow: 0 6px 20px rgba(0, 0, 0, .1); }
fieldset { border: 1px solid #e1e4ea; border-radius: 10px; margin: 0 0 10px; padding: 8px 10px 10px; }
legend { font-weight: 700; font-size: 14px; padding: 0 4px; }
.row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
input, select, button { font: inherit; font-size: 14px; }
input { width: 100px; padding: 6px; border: 1px solid #cbd2dc; border-radius: 6px; }
select { padding: 6px; border: 1px solid #cbd2dc; border-radius: 6px; }
button { padding: 7px 10px; border: 0; border-radius: 7px; background: #1d4ed8; color: #fff; cursor: pointer; }
.note { font-size: 12px; color: #9a3412; margin-top: 6px; min-height: 1em; }
.result { font-size: 22px; font-weight: 700; margin-top: 4px; }
.result small { font-size: 14px; font-weight: 400; color: #4b5563; }
</style>
</head>
<body>
<div class="card">
<fieldset>
<legend>Point A</legend>
<div class="row">
<input id="aLat" type="number" step="any" value="51.5074" aria-label="A latitude">
<input id="aLon" type="number" step="any" value="-0.1278" aria-label="A longitude">
<button id="me" type="button">Use my location</button>
</div>
<div class="note" id="note"></div>
</fieldset>
<fieldset>
<legend>Point B</legend>
<div class="row">
<input id="bLat" type="number" step="any" value="48.8566" aria-label="B latitude">
<input id="bLon" type="number" step="any" value="2.3522" aria-label="B longitude">
<select id="preset" aria-label="Sample places">
<option value="48.8566,2.3522">Paris</option>
<option value="40.7128,-74.0060">New York</option>
<option value="37.5665,126.9780">Seoul</option>
<option value="-33.8688,151.2093">Sydney</option>
</select>
</div>
</fieldset>
<div class="result" id="result"></div>
</div>
<script>
// great-circle distance on a sphere, in kilometres
function haversine(lat1, lon1, lat2, lon2) {
const R = 6371; // mean Earth radius in km
const rad = (d) => d * Math.PI / 180;
const dLat = rad(lat2 - lat1);
const dLon = rad(lon2 - lon1);
const h = Math.sin(dLat / 2) ** 2 +
Math.cos(rad(lat1)) * Math.cos(rad(lat2)) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(h));
}
const $ = (id) => document.getElementById(id);
function update() {
const km = haversine(+$('aLat').value, +$('aLon').value, +$('bLat').value, +$('bLon').value);
$('result').innerHTML = km.toFixed(1) + ' km <small>(' + (km * 0.621371).toFixed(1) + ' miles)</small>';
}
document.querySelectorAll('input').forEach((el) => el.addEventListener('input', update));
$('preset').addEventListener('change', () => {
const [lat, lon] = $('preset').value.split(',');
$('bLat').value = lat;
$('bLon').value = lon;
update();
});
$('me').addEventListener('click', () => {
$('note').textContent = 'Asking...';
navigator.geolocation.getCurrentPosition((pos) => {
$('aLat').value = pos.coords.latitude.toFixed(5);
$('aLon').value = pos.coords.longitude.toFixed(5);
$('note').textContent = '';
update();
}, (err) => {
// keep the sample point so the calculator still works
$('note').textContent = 'Error ' + err.code + ': ' + err.message.replace(/\.$/, '') + '. Point A stays on the sample (London).';
}, { timeout: 10000 });
});
update();
</script>
</body>
</html>
function haversine(lat1, lon1, lat2, lon2) {
const R = 6371; // km
const rad = (d) => d * Math.PI / 180;
const h = Math.sin(rad(lat2 - lat1) / 2) ** 2 +
Math.cos(rad(lat1)) * Math.cos(rad(lat2)) * Math.sin(rad(lon2 - lon1) / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(h));
}
London to Paris comes out at 343.6 km. That is the distance as the crow flies, not by road. For a walking or driving distance you need a routing service. For "which is closer", the straight line is usually enough.
A finished example: the nearest store
Put the two together and you have a store finder. Keep the store coordinates in an array, compute the distance from the user to each, and sort.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nearest store finder</title>
<style>
body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
.card { max-width: 480px; padding: 16px 18px; border-radius: 12px; background: #fff; box-shadow: 0 6px 20px rgba(0, 0, 0, .1); }
h2 { margin: 0 0 4px; font-size: 18px; }
.why { margin: 0 0 12px; font-size: 13px; color: #4b5563; }
.row { display: flex; flex-wrap: wrap; gap: 8px; }
button { font: inherit; font-size: 14px; padding: 8px 12px; border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; cursor: pointer; }
button.alt { background: #e5e7eb; color: #1d2330; }
button[aria-pressed="true"] { background: #0f5132; }
.status { margin: 10px 0; font-size: 13px; padding: 7px 9px; border-radius: 7px; background: #eef1f5; overflow-wrap: anywhere; }
.status.err { background: #fde2da; color: #9a3412; }
ol { list-style: none; margin: 0; padding: 0; }
li { display: flex; justify-content: space-between; gap: 10px; padding: 9px 10px; border-radius: 8px; font-size: 14px; }
li + li { border-top: 1px solid #eef1f5; }
li:first-child.ready { background: #d6f2df; }
li .d { font-variant-numeric: tabular-nums; color: #374151; white-space: nowrap; }
</style>
</head>
<body>
<div class="card">
<h2>Find the nearest store</h2>
<p class="why">Your location is only used on this page, to sort the list. It is not sent anywhere.</p>
<div class="row">
<button id="me" type="button">Use my location</button>
<button id="follow" type="button" aria-pressed="false">Follow me</button>
<button id="sample" type="button" class="alt">Use sample position</button>
</div>
<div class="status" id="status">Pick a starting point.</div>
<ol id="list"></ol>
</div>
<script>
// sample data: store names and coordinates (latitude, longitude)
const stores = [
{ name: 'Covent Garden', lat: 51.5117, lon: -0.1240 },
{ name: 'Camden', lat: 51.5390, lon: -0.1426 },
{ name: 'Shoreditch', lat: 51.5265, lon: -0.0786 },
{ name: 'Greenwich', lat: 51.4826, lon: 0.0077 },
{ name: 'Brixton', lat: 51.4613, lon: -0.1156 },
{ name: 'Notting Hill', lat: 51.5094, lon: -0.1966 },
{ name: 'Canary Wharf', lat: 51.5054, lon: -0.0235 }
];
function haversine(lat1, lon1, lat2, lon2) {
const rad = (d) => d * Math.PI / 180;
const h = Math.sin(rad(lat2 - lat1) / 2) ** 2 +
Math.cos(rad(lat1)) * Math.cos(rad(lat2)) * Math.sin(rad(lon2 - lon1) / 2) ** 2;
return 2 * 6371000 * Math.asin(Math.sqrt(h)); // metres
}
const list = document.getElementById('list');
const status = document.getElementById('status');
const followBtn = document.getElementById('follow');
let watchId = null;
function render(lat, lon, label) {
const sorted = stores
.map((s) => ({ ...s, m: haversine(lat, lon, s.lat, s.lon) }))
.sort((a, b) => a.m - b.m);
list.innerHTML = '';
sorted.forEach((s) => {
const li = document.createElement('li');
li.className = 'ready';
const d = s.m < 1000 ? Math.round(s.m) + ' m' : (s.m / 1000).toFixed(1) + ' km';
li.innerHTML = '<span>' + s.name + '</span><span class="d">' + d + '</span>';
list.appendChild(li);
});
status.className = 'status';
status.textContent = label;
}
function fail(err) {
status.className = 'status err';
status.textContent = 'Error ' + err.code + ': ' + err.message.replace(/\.$/, '') +
'. Try "Use sample position" to see the list.';
// a watch keeps running after errors 2 and 3; after 1 it never succeeds
if (err.code === err.PERMISSION_DENIED) stopFollowing();
}
function stopFollowing() {
if (watchId !== null) navigator.geolocation.clearWatch(watchId); // stop the GPS
watchId = null;
followBtn.setAttribute('aria-pressed', 'false');
followBtn.textContent = 'Follow me';
}
document.getElementById('me').addEventListener('click', () => {
status.className = 'status';
status.textContent = 'Asking for your location...';
navigator.geolocation.getCurrentPosition(
(p) => render(p.coords.latitude, p.coords.longitude,
'From your location (accuracy ' + Math.round(p.coords.accuracy) + ' m).'),
fail,
{ timeout: 10000, maximumAge: 60000 } // a position up to 1 minute old is fine
);
});
followBtn.addEventListener('click', () => {
if (watchId !== null) { stopFollowing(); return; }
watchId = navigator.geolocation.watchPosition(
(p) => render(p.coords.latitude, p.coords.longitude,
'Following you. Updated ' + new Date(p.timestamp).toLocaleTimeString() + '.'),
fail,
{ timeout: 15000 }
);
followBtn.setAttribute('aria-pressed', 'true');
followBtn.textContent = 'Stop following';
});
document.getElementById('sample').addEventListener('click', () => {
stopFollowing();
render(51.5008, -0.1246, 'From the sample position: Westminster, London.');
});
</script>
</body>
</html>
Follow me uses watchPosition. It takes the same arguments as getCurrentPosition, but calls the success function again each time the position changes. It returns an id. Pass that id to clearWatch to stop.
const id = navigator.geolocation.watchPosition(onMove, onError);
// later, when the user no longer needs live updates
navigator.geolocation.clearWatch(id);
A watch that is never cleared keeps the location hardware working, and that costs battery. Stop it when the user leaves the view or presses stop. In the error function, stop on error 1; errors 2 and 3 can be followed by a good position.
Inside an iframe: allow="geolocation"
Geolocation is controlled by permissions policy. By default only the top-level page and frames from the same origin may use it. A frame from another origin, or a sandboxed frame, needs allow="geolocation" on its <iframe> tag.

<iframe src="https://other.example/finder.html" allow="geolocation"></iframe>
Without it, the call fails with error 1 and no prompt is shown, even if the user already allowed the site.
None of the sandbox flags is about geolocation, so a sandboxed frame needs the allow attribute too. See the sandbox attribute and the iframe guide.
Fullscreen uses the same mechanism with allow="fullscreen", covered in the Fullscreen API guide. If you only need to show a fixed place rather than find the user, embedding a Google Map needs no permission at all.
When it does not work
| What you see | Cause | Fix |
|---|---|---|
Works on localhost, fails on an http:// address |
Not a secure context | Serve over HTTPS, or test on localhost |
| Error 1 at once, no prompt, on HTTPS | The user blocked the site earlier | Ask them to allow it in site settings; offer a fallback |
| Error 1 at once, inside an iframe | The frame lacks allow="geolocation" |
Add it to the <iframe> tag in the parent page |
| Nothing happens for a long time, then error 3 | No fix indoors or with high accuracy on | Set a timeout; retry without enableHighAccuracy |
| No callback at all | timeout left at Infinity |
Always pass a timeout |
| The same old position comes back | maximumAge is large |
Use maximumAge: 0 for a fresh position |
| Battery drains while the page is open | watchPosition never stopped |
Call clearWatch(id) when done |
Share it as a link
A store finder or a distance tool is easier to try than to describe. A screenshot shows one result, and an .html attachment may not open on a phone at all.
To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.
The page renders as written and its scripts run, so the people you send it to can press the buttons themselves. If you change the code later, the same link shows the new version.
Keep the sample-coordinates button in whatever you share. If the page ends up inside a frame without the geolocation permission, readers can still try everything else.