Geolocation & Device APIs
Modern browsers expose a rich set of device capabilities through the global navigator object — from a user’s physical location to the system clipboard, native share sheet, and haptic feedback. These APIs let web apps feel native, but every one of them is gated behind a permission prompt or a secure context (HTTPS) because they touch sensitive hardware and personal data. This page covers the Geolocation API in depth, then tours the most useful companion device APIs and the permission model that ties them together.
The Geolocation API
The Geolocation API lives at navigator.geolocation and reports the device’s position using GPS, Wi-Fi, cell towers, or IP — the browser picks the best available source. It is asynchronous and callback-based: you supply a success handler and an error handler. Geolocation only works in a secure context (HTTPS or localhost), and the browser always asks the user for permission the first time.
Getting the current position once
getCurrentPosition() takes a success callback, an optional error callback, and an optional options object. The success callback receives a GeolocationPosition whose coords property holds the data you care about.
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude, accuracy } = position.coords;
console.log(`You are at ${latitude}, ${longitude} (±${accuracy}m)`);
},
(error) => {
console.error(`Geolocation failed: ${error.message}`);
},
{ enableHighAccuracy: true, timeout: 10_000, maximumAge: 0 }
);
Output:
You are at 37.7749, -122.4194 (±20m)
Because the API predates promises, it is common to wrap it so you can await a position:
const getPosition = (options) =>
new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, options);
});
try {
const pos = await getPosition({ enableHighAccuracy: true });
console.log(pos.coords.latitude, pos.coords.longitude);
} catch (err) {
console.error("Could not read location:", err.message);
}
Position options
The third argument fine-tunes accuracy, freshness, and how long you are willing to wait.
| Option | Type | Default | Description |
|---|---|---|---|
enableHighAccuracy | boolean | false | Request the most precise source (GPS), at the cost of battery and speed. |
timeout | number (ms) | Infinity | Max time to wait before firing the error callback with TIMEOUT. |
maximumAge | number (ms) | 0 | Accept a cached position up to this age instead of fetching fresh. |
Watching position changes
For navigation or live tracking, watchPosition() invokes your success callback every time the device moves significantly. It returns a numeric watch ID that you pass to clearWatch() to stop listening — always clean up to save battery.
const watchId = navigator.geolocation.watchPosition(
(pos) => updateMap(pos.coords.latitude, pos.coords.longitude),
(err) => console.warn(err.message),
{ enableHighAccuracy: true, maximumAge: 5_000 }
);
// Later, when the view is closed:
navigator.geolocation.clearWatch(watchId);
Handling errors
The error callback receives a GeolocationPositionError with a code you should branch on, since a denied permission needs very different UX from a timeout.
function onError(error) {
switch (error.code) {
case error.PERMISSION_DENIED:
return showMessage("Enable location access to use this feature.");
case error.POSITION_UNAVAILABLE:
return showMessage("Location signal is unavailable right now.");
case error.TIMEOUT:
return showMessage("Locating took too long — please retry.");
}
}
Calling
getCurrentPosition()onhttp://(anything butlocalhost) silently fails with aPERMISSION_DENIEDerror in modern browsers. Serve over HTTPS during development with tools like a local TLS proxy or your framework’s--httpsflag.
A live geolocation demo
<button id="locate">Find my location</button>
<p id="result">Click the button and approve the prompt.</p>
<script>
const out = document.getElementById("result");
document.getElementById("locate").addEventListener("click", () => {
if (!("geolocation" in navigator)) {
out.textContent = "Geolocation is not supported here.";
return;
}
out.textContent = "Locating…";
navigator.geolocation.getCurrentPosition(
({ coords }) => {
out.textContent =
`Lat: ${coords.latitude.toFixed(4)}, ` +
`Lng: ${coords.longitude.toFixed(4)} (±${Math.round(coords.accuracy)}m)`;
},
(err) => { out.textContent = `Error: ${err.message}`; },
{ enableHighAccuracy: true, timeout: 10000 }
);
});
</script>
Checking permissions ahead of time
The Permissions API lets you read the current state of a permission without triggering a prompt, so you can tailor your UI. Query "geolocation" and inspect the state, which is "granted", "denied", or "prompt".
const status = await navigator.permissions.query({ name: "geolocation" });
console.log(status.state); // "prompt" | "granted" | "denied"
status.addEventListener("change", () => {
console.log("Permission changed to", status.state);
});
Output:
prompt
Other navigator & device APIs
Beyond location, navigator surfaces several focused device capabilities. All require a secure context, and most need a transient user activation — they must run inside a click or tap handler, not on page load.
Clipboard API
The async Clipboard API reads and writes text (and richer data) with navigator.clipboard. Writing usually works inside a user gesture; reading prompts for permission.
copyBtn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText("npm install devcraftly");
console.log("Copied!");
} catch {
console.error("Clipboard write was blocked.");
}
});
Web Share API
navigator.share() opens the operating system’s native share sheet, returning a promise that resolves when the user finishes. Feature-detect it, since desktop browsers often lack it.
if (navigator.share) {
await navigator.share({
title: "DevCraftly",
text: "Great JS docs!",
url: location.href,
});
}
Vibration API
On supported mobile devices, navigator.vibrate() triggers haptic feedback. Pass a duration in milliseconds or a pattern array of vibrate/pause durations; pass 0 or [] to cancel.
<button id="buzz">Buzz me</button>
<p id="note"></p>
<script>
document.getElementById("buzz").addEventListener("click", () => {
const note = document.getElementById("note");
if (navigator.vibrate) {
navigator.vibrate([200, 100, 200]); // buzz, pause, buzz
note.textContent = "Vibration triggered (mobile only).";
} else {
note.textContent = "Vibration API not supported on this device.";
}
});
</script>
Quick capability reference
| API | Entry point | Secure context | Needs user gesture |
|---|---|---|---|
| Geolocation | navigator.geolocation | Yes | No (but prompts) |
| Clipboard | navigator.clipboard | Yes | Usually |
| Web Share | navigator.share() | Yes | Yes |
| Vibration | navigator.vibrate() | No | Yes |
Best practices
- Always feature-detect (
"geolocation" in navigator,navigator.share) before calling — support varies widely across devices and browsers. - Request location only in response to a user action and explain why first; unsolicited prompts get denied.
- Use
enableHighAccuracy: truesparingly; it drains battery and is slower, so prefer it only for active navigation. - Always pair
watchPosition()withclearWatch()when the view unmounts to stop background GPS usage. - Set a sensible
timeoutand handle everyGeolocationPositionErrorcode with a distinct, actionable message. - Use the Permissions API to detect a
"denied"state and guide users to re-enable access instead of repeatedly failing. - Never block core functionality behind a device API — provide a manual fallback (e.g., a postal-code input) when permission is refused.