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-device-motion

v1.0.0-beta.9

Published

Reactive device acceleration and rotation from DeviceMotionEvent

Readme

browser-device-motion

Wraps the devicemotion event as read-only state: linear acceleration, acceleration including gravity and rotation rate, plus derived selectors. Readings are usually null on desktops, which have no motion sensors.

Permission

Subscribing never prompts. Where the browser gates motion behind a prompt (iOS Safari), call requestMotionPermission() directly from a click handler, with no await before it. The specification checks the gesture's transient activation when the request starts; how a given browser presents or remembers the prompt has not been verified on iOS Safari.

Install

bun add @valdres/browser-device-motion

Live example

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

Usage

Every read works through a store, with no adapter at all:

import { store } from "valdres"
import {
    accelerationSelector,
    motionStatusAtom,
    permissionAtom,
    requestMotionPermission,
} from "@valdres/browser-device-motion"

const app = store()
app.get(permissionAtom) // "prompt" on iOS until answered
button.addEventListener("click", () => {
    // Called inside the click: no `await` before it.
    void requestMotionPermission()
})
const stop = app.sub(accelerationSelector, () => {
    draw(app.get(accelerationSelector)) // Vector3 | null
})
app.get(motionStatusAtom) // "active" while a store listens
stop()
import { useValue } from "valdres-react"
import {
    accelerationSelector,
    permissionAtom,
    requestMotionPermission,
    rotationRateSelector,
} from "@valdres/browser-device-motion"

function Motion() {
    const permission = useValue(permissionAtom)
    const accel = useValue(accelerationSelector) // Vector3 | null
    const rot = useValue(rotationRateSelector) // RotationRateSnapshot | null
    return (
        <div>
            {permission === "prompt" && (
                <button onClick={() => void requestMotionPermission()}>Enable motion</button>
            )}
            <pre>{JSON.stringify({ accel, rot }, null, 2)}</pre>
        </div>
    )
}

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 | | -------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------- | | motionAtom | selector (read-only) | MotionSnapshot \| null | | motionStatusAtom | selector (read-only) | MotionStatus | | permissionAtom | external atom (read-only) | PermissionValue | | accelerationSelector | selector | Vector3 \| null | | accelerationIncludingGravitySelector | selector | Vector3 \| null | | accelerationMagnitudeSelector | selector | number \| null | | rotationRateSelector | selector | RotationRateSnapshot \| null | | intervalSelector | selector | number \| null | | requestMotionPermission | util fn | () => Promise<PermissionValue> | | MotionSnapshot | type | { acceleration, accelerationIncludingGravity, rotationRate: ... \| null; interval: number; timeStamp: number } | | Vector3 | type | { x, y, z: number \| null } | | RotationRateSnapshot | type | { alpha, beta, gamma: number \| null } | | MotionStatus | type | "unsupported" \| "insecure" \| "idle" \| "active" | | PermissionValue | type | "granted" \| "denied" \| "prompt" \| "unsupported" |

Ownership

Motion belongs to the device, so every export is read-only: stores cannot set, reset or update them, at compile time or at runtime. motionAtom and motionStatusAtom project one source, so a reading and the status it was published with are never observed apart.

motionStatusAtom is:

  • "unsupported" without a window or a DeviceMotionEvent (servers, workers, browsers without the API);
  • "insecure" when window.isSecureContext is false — the API is restricted to secure contexts, so nothing is attached;
  • "idle" while no store tree listens;
  • "active" while at least one store tree listens. Readings can still be null: no sensor, permission not granted yet, or a hidden page (browsers only fire devicemotion for visible documents).

Permission

permissionAtom never prompts. Its value is:

  • the newest answer this page has seen, shared by every store: a completed requestMotionPermission(), a Permissions API answer or a later change event — an answer whose operation started earlier never overwrites a newer one;
  • "granted" observed from a devicemotion reading with values, as evidence of access rather than an authoritative answer. Browsers only deliver readings once access is granted, so a reading may fill an unknown or "prompt" state — this is how the package learns a grant given through @valdres/browser-device-orientation, since iOS asks once for both. It never overrides a recorded denial; only a newer request, query answer or change event can recover from one. All-null readings, which Chromium sends without access, prove nothing;
  • while a store subscribes, the Permissions API's combined answer for accelerometer and gyroscope where the engine knows those names (Chromium), followed through its change events while a store subscribes;
  • otherwise "prompt" where DeviceMotionEvent.requestPermission exists, and "granted" where it does not (those browsers fire events without asking);
  • "unsupported" without the API or in an insecure context — nothing can be asked there. The status keeps the two apart ("unsupported" vs "insecure"), and a denial is "denied", never "unsupported".

requestMotionPermission() is the only call that can show a prompt. It calls DeviceMotionEvent.requestPermission() synchronously, inside the caller's task, so a call made directly from a click handler — with no await before it — reaches the browser while that click's transient activation is still current, which is when the specification checks it. That is all the package guarantees: whether a browser then prompts, and how long it remembers the answer, is the browser's behaviour. Verified in Chromium 151 and 154 (headless, with emulated permission states); the iOS Safari prompt flow is not yet verified:

| Outcome | Resolves | Recorded | | ------------------------------------------------------------- | ------------------------ | ------------------------------ | | The user grants or denies | "granted" / "denied" | yes, for every store | | No transient activation (NotAllowedError) or a host failure | the current value | no — a later gesture can retry | | The browser has no requestPermission | the current value | no | | No API, or an insecure context | "unsupported" | no |

Concurrent calls share one request. A browser prompt cannot be cancelled; a request that completes after every store was disposed still records its answer, which later permission updates replace.

Errors. The promise never rejects and the call never throws: it settles with the browser's answer (or the current value, as above). If a store subscriber throws while that answer is published, every store is still notified, the answer is still recorded and returned, and the subscriber failure is reported separately through globalThis.reportError — or thrown in a later task where there is none — the way core reports onCommit failures. The request is never retried. A subscriber that throws on a Permissions API answer is reported the same way; one that throws on a change event is rethrown from that event's listener, which the browser reports.

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.

Server rendering

The server snapshots are fixed seeds: motionStatusAtom is "idle", motionAtom is null and permissionAtom is "prompt". useValue renders them first and switches to the live values after hydration. Nothing attaches, queries or prompts during a server render.

Lifetime

One devicemotion listener per window is shared by every store tree that retains the source and removed with the last one — when the last retaining subscriber leaves or its store is disposed. Child scopes share their root's registration. The window's latest reading is cached while attached and discarded on detach, so a later subscription never reports a stale reading. Importing the package and reading without a subscription attach nothing.

Every store tree is invalidated for each reading even when another tree's subscriber throws; the failures are rethrown afterwards from the native listener, where the browser reports them.


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