# Generic Sensor API

> Accelerometer, Gyroscope, orientation, Magnetometer, and AmbientLightSensor on one Sensor base. Options, error event names, Chromium frequency caps, fallbacks.

The Generic Sensor API is one `Sensor` base interface (`start()`, `stop()`, `reading`, `activate`, and `error` events) that concrete device sensors extend: `Accelerometer`, `LinearAccelerationSensor`, `GravitySensor`, `Gyroscope`, `AbsoluteOrientationSensor`, `RelativeOrientationSensor`, `Magnetometer`, and `AmbientLightSensor`. Pages construct a concrete class, call `start()`, and read the latest sample from its attributes inside a `reading` handler; readings are only delivered to a visible, focused document in a secure context.

Support is a Chromium-only position with a split inside it. Chrome 67 (Edge 79, Samsung Internet 9.0, Android WebView 67) ships `Accelerometer`, `LinearAccelerationSensor`, `Gyroscope`, `AbsoluteOrientationSensor`, and `RelativeOrientationSensor` without a flag, and Chrome 91 added `GravitySensor`. `Magnetometer` and `AmbientLightSensor` exist since Chrome 56 only behind `chrome://flags/#enable-experimental-web-platform-features`. Firefox and Safari implement none of these interfaces (BCD `api.Accelerometer`, `api.Magnetometer`, `api.AmbientLightSensor`); the [DeviceMotionEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent) path is the cross-engine fallback for motion data.

## Syntax

```js
new Accelerometer()
new Accelerometer(options)
new Gyroscope(options)
new AbsoluteOrientationSensor(options)
new Magnetometer(options)
new AmbientLightSensor(options)

sensor.start()
sensor.stop()
```

Constructors are synchronous and return an idle sensor. `start()` and `stop()` return `undefined`; activation happens asynchronously and is signalled by the `activate` event, after which `activated` is `true`, `hasReading` becomes `true` on the first sample, and `timestamp` carries the sample's `DOMHighResTimeStamp`. Motion sensors expose `x`, `y`, `z` (m/s² for accelerometers, rad/s for `Gyroscope`, µT for `Magnetometer`); orientation sensors expose `quaternion` plus `populateMatrix(target)`; `AmbientLightSensor` exposes `illuminance` in lux. Every interface is `[SecureContext]`.

## Parameters

Each constructor takes one optional `options` dictionary. The base `SensorOptions` has one member; the motion and orientation sensors extend it with `referenceFrame`.

| Member | Type | Required | Applies to | Description |
|---|---|---|---|---|
| `frequency` | `double` (Hz) | No | All sensors | Requested sampling and reporting rate. The specification makes it a hint: the browser may clamp it to the platform sensor's limits. Chromium caps accelerometer, gyroscope, and orientation sensors at 60 Hz (default 10 Hz) and `Magnetometer` and `AmbientLightSensor` at 10 Hz (`sensor_traits.h`). |
| `referenceFrame` | `"device"` or `"screen"` | No, defaults to `"device"` | `Accelerometer`, `LinearAccelerationSensor`, `GravitySensor`, `Gyroscope`, `Magnetometer`, both orientation sensors | Coordinate system for the readings. `"screen"` rotates the axes with the current screen orientation so a landscape tablet still reports "up" as positive y. |

Readings are quantized before they reach the page: the Accelerometer specification rounds `x`, `y`, and `z` to the nearest 0.1 m/s², a fingerprinting mitigation that also sets the floor for how small a motion you can detect.

## Exceptions

The constructor throws synchronously; `start()` does not throw and reports failure through an `error` event whose `event.error` is the `DOMException` below.

| Exception | Where | Condition |
|---|---|---|
| `SecurityError` | Constructor | The document is not allowed to use the sensor's policy-controlled feature (`accelerometer`, `gyroscope`, `magnetometer`, or `ambient-light-sensor`; orientation sensors need `accelerometer` plus `gyroscope`, and the absolute one also `magnetometer`). A cross-origin iframe needs these in its `allow` attribute. |
| `NotSupportedError` | Constructor | `options` contains a key the sensor type does not support (the "initialize a sensor object" step). |
| `NotAllowedError` | `error` event | The sensor permission request resolved to `"denied"`. Chromium ties these permissions to the `accelerometer`, `gyroscope`, `magnetometer`, and `ambient-light-sensor` names in `navigator.permissions.query()`. |
| `NotReadableError` | `error` event | "Connect to sensor" failed: the device has no such sensor, the platform refused it, or the sensor stopped mid-use. The sensor returns to `"idle"` and stays silent until `start()` is called again. |

A `ReferenceError` from `new Magnetometer()` is not a sensor error; it means the constructor is not defined in this browser, which is the case to feature-detect, not to catch.

:::observed
Chrome clamps an over-range `frequency` rather than rejecting it and says so in the DevTools Console at Info level: `new Accelerometer({ frequency: 120 })` logs `Maximum allowed frequency value for this sensor type is 60 Hz.` and runs at 60 Hz. When a device lacks the hardware, the `error` event's `event.error.message` is `Could not connect to a sensor` (name `NotReadableError`), and a `start()` that the platform rejects reports `start() call has failed.` All three strings are in Chromium's [`sensor.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/sensor/sensor.cc) and [`sensor_proxy.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/sensor/sensor_proxy.cc) (chromium.googlesource.com).
:::

## Examples

The examples feature-detect the exact constructor they need and fall back to `DeviceMotionEvent`, which Firefox and Safari do implement, or to a static UI when neither path is available.

### Shake detection with an Accelerometer and a DeviceMotionEvent fallback

The accelerometer path is preferred because it delivers calibrated, quantized samples at a rate you choose. Where the constructor is missing the same threshold runs on `devicemotion`, which reports unquantized `accelerationIncludingGravity`.

```js
const SHAKE_THRESHOLD = 25; // m/s^2, total magnitude

function onShake(callback) {
  if ('Accelerometer' in window) {
    let sensor;
    try {
      sensor = new Accelerometer({ frequency: 30 });
    } catch (err) {
      console.warn(`${err.name}: ${err.message}`); // blocked by Permissions Policy
      return false;
    }
    sensor.addEventListener('error', (event) => {
      console.warn(`${event.error.name}: ${event.error.message}`);
    });
    sensor.addEventListener('reading', () => {
      if (Math.hypot(sensor.x, sensor.y, sensor.z) > SHAKE_THRESHOLD) callback();
    });
    sensor.start();
    return true;
  }

  if ('DeviceMotionEvent' in window) {
    window.addEventListener('devicemotion', (event) => {
      const a = event.accelerationIncludingGravity;
      if (a && Math.hypot(a.x, a.y, a.z) > SHAKE_THRESHOLD) callback();
    });
    return true;
  }

  return false; // no motion data: show a button instead
}

if (!onShake(() => document.body.classList.toggle('shaken'))) {
  document.querySelector('#shake-button').hidden = false;
}
```

On iOS 13 and later `devicemotion` is itself permission-gated through `DeviceMotionEvent.requestPermission()`, which must be called from a user gesture; the fallback branch therefore belongs behind a tap in a real app.

### Checking permission before constructing an orientation sensor

`AbsoluteOrientationSensor` depends on three permissions at once. Querying them first lets the page explain what is blocked instead of waiting for a `NotAllowedError` on the `error` event.

```js
async function startCompass(onHeading) {
  if (!('AbsoluteOrientationSensor' in window)) return 'unsupported';

  const names = ['accelerometer', 'gyroscope', 'magnetometer'];
  const states = await Promise.all(
    names.map((name) => navigator.permissions.query({ name }).then((s) => s.state)),
  );
  if (states.includes('denied')) return 'denied';

  const sensor = new AbsoluteOrientationSensor({ frequency: 10, referenceFrame: 'screen' });
  sensor.addEventListener('reading', () => {
    const [qx, qy, qz, qw] = sensor.quaternion;
    const yaw = Math.atan2(2 * (qw * qz + qx * qy), 1 - 2 * (qy * qy + qz * qz));
    onHeading(((yaw * 180) / Math.PI + 360) % 360);
  });
  sensor.addEventListener('error', (event) => {
    if (event.error.name === 'NotReadableError') onHeading(null); // no magnetometer on this device
  });
  sensor.start();
  return 'started';
}
```

Returning a string lets the caller render three different states: a compass, a "turn on motion access" message, or a plain north-up map when the API is absent.

## See also

- [Geolocation API](/reference/capabilities/geolocation/), the other position-related permission with a `PERMISSION_DENIED` path
- [Idle Detection API](/reference/capabilities/idle-detection/), another Chromium-only capability gated by the Permissions API
- [Screen Wake Lock API](/reference/capabilities/wake-lock/), often paired with motion tracking screens
- [Generic Sensor API: Sensor.start()](https://www.w3.org/TR/generic-sensor/#sensor-start) (w3.org)
- [Generic Sensor API: Mitigation strategies](https://www.w3.org/TR/generic-sensor/#mitigation-strategies) (w3.org)
- [Accelerometer: reading quantization algorithm](https://www.w3.org/TR/accelerometer/#accelerometer-reading-quantization-algorithm) (w3.org)
- [Sensors for the web](https://developer.chrome.com/docs/capabilities/web-apis/generic-sensor) (developer.chrome.com)