This is part 4 of FieldKit, a series where I build one real Progressive Web App and use it to dig into what modern PWAs can actually do. FieldKit is a field-notes app — open source (on GitHub). It already works offline, installs, and captures photos and audio. Now we answer two questions every field note should carry: where was it taken, and which way was I facing?
Location turns a note into a place
A photo of a trail junction is useful. A photo of a trail junction pinned to 46.812, 8.224 and tagged "facing NE" is a record. For a field tool, location is the metadata that makes everything else searchable later.
The web gives you two relevant sensors here:
- Geolocation — latitude/longitude (and accuracy), via the Geolocation API.
- Orientation — a compass heading, derived from the device's magnetometer via device-orientation events.
They look similar from the outside ("ask the device where/how it is") but they behave very differently in practice. Let's take them one at a time.
Geolocation: one call, three things to respect
Getting a position is a single call. Doing it well means respecting three things: accuracy, timeouts, and permission.
export function getPosition(
options = { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
) {
if (!("geolocation" in navigator)) {
return Promise.reject(new Error("Geolocation isn't available here."));
}
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(pos) =>
resolve({
lat: pos.coords.latitude,
lng: pos.coords.longitude,
accuracy: pos.coords.accuracy, // metres
}),
(err) => reject(new Error(describeGeoError(err))),
options
);
});
}
Those options matter more than people assume:
-
enableHighAccuracy: trueasks for GPS-grade precision instead of a coarse Wi-Fi/IP estimate. It costs battery and time — use it for a field app, skip it for "which city is this user in." -
timeoutcaps how long you'll wait. Without it, a device that can't get a fix leaves your promise hanging forever. -
maximumAge: 0forbids a cached position — we want now, not where the phone was ten minutes ago.
And accuracy isn't decoration: it's the radius in metres the browser is confident about. A reading with accuracy: 2000 is a Wi-Fi guess, not a GPS lock — worth surfacing to the user rather than pretending 5 decimal places is truth.
Handle every failure mode
Geolocation fails in distinct ways, and users deserve a specific message for each:
function describeGeoError(err) {
switch (err.code) {
case err.PERMISSION_DENIED: return "Location permission denied.";
case err.POSITION_UNAVAILABLE: return "Location unavailable right now.";
case err.TIMEOUT: return "Timed out getting your location.";
default: return "Couldn't get your location.";
}
}
The compass: welcome to sensor fragmentation
Heading is where the web gets genuinely messy, and pretending otherwise is how you ship a broken compass. There's no clean "give me north" API. You listen to device-orientation events and derive a heading — and every platform does it slightly differently.
Two big forks:
1. iOS gates it behind a permission call that needs a user gesture. Since iOS 13, you must call DeviceOrientationEvent.requestPermission() from inside a click/tap handler, or you get nothing:
if (
typeof DeviceOrientationEvent !== "undefined" &&
typeof DeviceOrientationEvent.requestPermission === "function"
) {
const state = await DeviceOrientationEvent.requestPermission(); // iOS gate
if (state !== "granted") throw new Error("Compass permission denied.");
}
2. The heading value lives in different places. iOS hands you a ready-made webkitCompassHeading (degrees clockwise from north). Everyone else fires an absolute orientation event where you derive heading from alpha — and some browsers name that event deviceorientationabsolute rather than deviceorientation. So you listen for both and read whichever field exists:
const onOrient = (e) => {
let heading;
if (typeof e.webkitCompassHeading === "number") {
heading = e.webkitCompassHeading; // iOS
} else if (e.absolute && typeof e.alpha === "number") {
heading = (360 - e.alpha) % 360; // convert absolute alpha to heading
} else {
return; // no usable heading in this event — wait for the next one
}
finish(heading);
};
window.addEventListener("deviceorientationabsolute", onOrient);
window.addEventListener("deviceorientation", onOrient);
And because plenty of devices have no magnetometer at all (most laptops), you need a timeout that gives up gracefully instead of waiting forever:
const timer = setTimeout(() => {
cleanup();
if (!settled) reject(new Error("No compass data on this device."));
}, timeout);
In FieldKit the heading is strictly a bonus: when the user attaches a location, I try to read a heading too, but a failure never blocks saving the note.
const pos = await getPosition();
let heading = null;
try {
heading = await getHeading();
} catch {
/* no magnetometer / denied — carry on without a heading */
}
pendingLocation = { lat: pos.lat, lng: pos.lng, heading };
That "best-effort sensor, required core data" split is the pattern I reach for with any flaky capability: never let the nice-to-have take down the must-have.
The big limitation: no background geolocation
Here's the thing to internalise, because it shapes what you can even build: the web cannot track location in the background. There is no web equivalent of a native app's "always allow" background location. getCurrentPosition and even watchPosition only run while your page is open and focused. Close the tab or lock the phone and location updates stop.
For FieldKit that's fine — we capture a position at the moment you save a note. But if you were dreaming of a web-based run tracker that records your route with the screen off, that's simply not a thing the web platform allows, by design, for privacy reasons. Know this before you promise it to anyone.
Honest support picture
-
Geolocation API: universal, including Safari and Firefox — but secure-context only (HTTPS or
localhost) and always permission-gated.watchPositionexists for live updates while the page is open. -
Device orientation / compass: broadly available on devices that have a magnetometer (i.e. phones), but the API surface is fragmented —
webkitCompassHeading(iOS) vs absolutealpha(others), plus the iOSrequestPermission()gate and the HTTPS requirement. Treat a heading as best-effort, never guaranteed. - No background geolocation anywhere on the web. Full stop.
- The newer Generic Sensor API (
AbsoluteOrientationSensor,Magnetometer) is a cleaner path on Chromium but isn't in Safari, so device-orientation events remain the portable choice today.
As always, verify on caniuse: Geolocation and DeviceOrientation before you depend on specifics.
How this compares to Electron
- Electron runs on desktops, which usually have no GPS and no magnetometer — so geolocation falls back to a coarse Wi-Fi/IP lookup and a compass heading basically isn't a thing. Where Electron wins is the background story: with Node and OS access it can keep working with location-ish data while minimised, run as a background process, and integrate with native location services the web can't touch.
- The PWA is the opposite trade-off, and for a field app it's the right one: the user is holding a phone that has real GPS and a real compass, and the same code that would give Electron a fuzzy desktop guess gives the PWA a precise, heading-aware fix. The cost is the platform's privacy guardrails — permission prompts, secure context, and no background tracking.
Put bluntly: for anything mobile and location-driven, the PWA has better hardware under it than a desktop Electron app, and pays for it only with the web's (reasonable) privacy constraints.
Try it
Serve FieldKit over localhost (or HTTPS on a phone), tap 📍, and grant location.
On a phone with a magnetometer you'll also get a heading like "🧭 NE 45°" on the note. On a laptop you'll get the position and a graceful "no compass" — exactly as designed.
git clone https://github.com/JohnJunior/FieldKit.git
cd FieldKit
npx serve .
Next up (FieldKit #5): getting data in and out — reading and writing real files with the File System Access API, sharing entries via the Web Share API, and the honest fallbacks for the browsers that support neither.

Top comments (0)