npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@valdres/browser-geolocation

v1.0.0-beta.9

Published

Reactive geolocation — position, accuracy, permission, and status

Readme

browser-geolocation

Wraps navigator.geolocation.watchPosition as read-only, Store-local state: the latest position, its status and error, the "geolocation" permission, and per-field selectors.

Explicit start

Reading or subscribing never starts a watch and never prompts. A watch starts only when you call watchGeolocation(store, options) — typically from a click — and that call may show the browser's location prompt. It returns the disposer that stops it.

Install

bun add @valdres/browser-geolocation

Live example

▶ Live example: https://valdres.dev/react/plugins/browser-geolocation

Usage

import { store } from "valdres"
import {
    coordsSelector,
    geolocationAtom,
    watchGeolocation,
} from "@valdres/browser-geolocation"

const app = store()
app.get(geolocationAtom) // { status: "idle", position: null, error: null }

let stop: (() => void) | undefined
startButton.addEventListener("click", () => {
    stop = watchGeolocation(app, { enableHighAccuracy: true })
})
app.sub(coordsSelector, () => {
    const coords = app.get(coordsSelector) // { latitude, longitude } | null
})
stopButton.addEventListener("click", () => stop?.())
import { useEffect, useState } from "react"
import { useStore, useValue } from "valdres-react"
import {
    coordsSelector,
    geolocationAtom,
    watchGeolocation,
} from "@valdres/browser-geolocation"

function Location() {
    const store = useStore()
    const [on, setOn] = useState(false)
    // The effect owns the watch: unmounting or turning it off stops it.
    useEffect(() => (on ? watchGeolocation(store) : undefined), [store, on])
    const { status, error } = useValue(geolocationAtom)
    const coords = useValue(coordsSelector)
    return (
        <div>
            <button onClick={() => setOn(!on)}>{on ? "Stop" : "Share location"}</button>
            {status} {coords && `${coords.latitude}, ${coords.longitude}`}
            {error && ` (${error.code}: ${error.message})`}
        </div>
    )
}

Starting the watch from an effect that a click enabled is fine: the specification does not require transient activation for geolocation. watchGeolocation calls watchPosition synchronously, so calling it inside a click handler hands the browser that click's activation too; whether a browser uses it is up to the browser.

Adapter support in this beta

valdres-react is the only adapter migrated to the v1 core. The Vue, Svelte, Solid and Angular adapters cannot read an external atom yet. Until they ship, read this package with store.get / store.sub as above.

Exports

| Export | Kind | Type | | ------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------ | | geolocationAtom | selector (read-only) | GeolocationState | | positionAtom | selector (read-only) | GeolocationSnapshot \| null | | geolocationStatusAtom | selector (read-only) | GeolocationStatus | | geolocationErrorAtom | selector (read-only) | GeolocationError \| null | | permissionAtom | external atom (read-only) | PermissionValue | | coordsSelector | selector | { latitude, longitude } \| null | | accuracySelector | selector | number \| null | | altitudeSelector | selector | number \| null | | altitudeAccuracySelector | selector | number \| null | | headingSelector | selector | number \| null | | speedSelector | selector | number \| null | | timestampSelector | selector | number \| null | | watchGeolocation | util fn | (store: Store, options?: GeolocationWatchOptions) => () => void | | GeolocationWatchConflictError | error class | thrown for conflicting options in one store | | GeolocationState | type | { status, position, error } | | GeolocationSnapshot | type | { latitude, longitude, accuracy, altitude, altitudeAccuracy, heading, speed, timestamp } | | GeolocationStatus | type | "unsupported" \| "insecure" \| "idle" \| "pending" \| "active" \| "denied" \| "error" | | GeolocationError | type | { code: 1 \| 2 \| 3; message: string } | | GeolocationWatchOptions | type | { enableHighAccuracy?, timeout?, maximumAge? } | | PermissionValue | type | "granted" \| "denied" \| "prompt" \| "unsupported" |

Watches belong to stores

watchGeolocation(store, options?) starts one navigator.geolocation.watchPosition for store and returns an idempotent disposer. Options default to enableHighAccuracy: false, timeout: 30_000 and maximumAge: 0; invalid values throw a TypeError before anything starts.

  • Store-local. The watch publishes into store and the scopes below it that do not run their own. Independent stores run independent watches with their own options, and never see each other's positions.
  • No silent overwrites. Callers that ask one store for the same options share its watch, which stops when the last disposer runs. Asking the same store for different options throws GeolocationWatchConflictError (active and requested carry both option sets). Give a second consumer its own scope — store.scope() — for a second watch.
  • Scopes. A child scope reads its parent's watch until it starts its own, and reads the parent's again when its own is released. Sibling scopes can all read the same parent watch — still one native watch. A child's own watch is visible only to that child and the scopes below it: it never changes what its parent or its siblings read, its options never conflict with theirs, and stopping or disposing it leaves the parent's watch running.
  • Cancellation. The disposer clears the native watch. Disposing the store clears every watch it owns. To change options, dispose and start again; a report from the old watch can never land in the new one, or in a store that stopped watching.
  • Phases. watchGeolocation throws StoreDisposedError for a disposed store and the core's capability errors inside a transaction or subscriber callback, before anything starts. A disposer called inside a transaction finishes in a microtask.

Status, position and error

geolocationAtom publishes all three together, so they are never observed apart; positionAtom, geolocationStatusAtom and geolocationErrorAtom project it.

| Status | Meaning | Position | Error | | --------------- | --------------------------------------------------------------------------------- | ------------------------- | --------------- | | "unsupported" | no navigator.geolocation | null | null | | "insecure" | isSecureContext is false; browsers refuse positions there, so no watch starts | null | null | | "idle" | no watch in this store or the scopes above it | null | null | | "pending" | a watch started and has not reported — the prompt may be showing | null | null | | "active" | the latest report was a position | the position | null | | "denied" | the browser refused the position and ended the watch | the last position, if any | code 1 | | "error" | the latest report was an ordinary error; the watch keeps running | the last position, if any | code 2 or 3 |

Error codes follow GeolocationPositionError. 1 (PERMISSION_DENIED: the user, a geolocation permissions policy in a cross-origin iframe, or the browser) is "denied" and ends the watch; recover by disposing and starting again once the user allows location. 2 (POSITION_UNAVAILABLE) and 3 (TIMEOUT) are "error" and keep the watch running: the next position turns the status back to "active". "unsupported", "insecure", "denied" and "error" are always distinct. Browsers only deliver positions to visible documents, so a hidden page stays "pending" or keeps its last report.

Permission

permissionAtom reports the "geolocation" permission and never prompts. Subscribing queries the Permissions API once per page and follows its change events, so it moves to "granted" or "denied" when a watch's prompt is answered. Before any answer, reads report "prompt". It is "unsupported" without the Permissions API, when the engine rejects the query, without navigator.geolocation, or in an insecure context.

Unsubscribed reads may be stale. While no store subscribes, nothing observes the permission: a read returns the last answer this page saw, which can be out of date if the user changed the setting meanwhile. Observation resumes with the next subscription, which queries again and corrects it. A remount therefore does not flicker back to "prompt".

Errors. A subscriber that throws while a Permissions API answer is published is reported through globalThis.reportError (or thrown in a later task), never left as an unhandled rejection; one that throws on a change event, or on a position or error the browser delivers to a watch, is rethrown from that browser callback, which the browser reports. Every store is notified first either way.

Server rendering

Reads never start anything, so a server render is inert: geolocationAtom is { status: "idle", position: null, error: null } and permissionAtom is "prompt". Start watches in effects or event handlers, which do not run on the server. useValue renders the seeds first and switches to the live values after hydration.

Lifetime

Each running watch is one native watchPosition, owned by its store and cleared by its last disposer or the store's disposal. The Permissions API change listener is shared by every store tree that subscribes to permissionAtom and removed with the last one. Importing the package and reading state attach, query and request nothing.


Full documentation: https://valdres.dev/react/plugins/browser-geolocation