# Idle Detection API

> IdleDetector reports whether the user is active or idle and whether the screen is locked. Permission model, 60-second minimum threshold, exceptions, examples.

`IdleDetector` tells a page whether the user has interacted with the device within a threshold you choose (at least one minute) and whether the screen is locked, firing a `change` event when either answer flips. It is a WICG draft gated by the `idle-detection` permission, meant for presence indicators in chat apps and for pausing expensive work when nobody is watching.

Support is Chromium only: Chrome 94 on desktop and Android, Edge 94 to 96 and again from Edge 114 (the interface was absent in between), Opera and Samsung Internet in the matching Chromium releases. Firefox and Safari have no implementation in any version (BCD `api.IdleDetector`).

## Syntax

```js
new IdleDetector()

IdleDetector.requestPermission()

idleDetector.start()
idleDetector.start(options)
```

The constructor takes no arguments. `requestPermission()` is a static method, `[Exposed=Window]` only, that returns a `Promise<PermissionState>` resolving to `"granted"`, `"denied"`, or `"prompt"`. `start()` returns a `Promise<undefined>` that resolves once the detector is `"started"`. The whole interface is `[SecureContext]` and is also exposed in dedicated workers, where `start()` works but `requestPermission()` does not exist.

## Parameters

`start()` takes one optional `options` argument, an `IdleOptions` dictionary.

| Member | Type | Required | Description |
|---|---|---|---|
| `threshold` | `unsigned long long` (`[EnforceRange]`) | No | Milliseconds of inactivity after which `userState` becomes `"idle"`. The minimum is 60,000; smaller values reject the promise. Negative or non-finite values throw a `TypeError` at the binding layer because of `[EnforceRange]`. |
| `signal` | `AbortSignal` | No | Aborting the signal stops the detector and rejects the pending `start()` promise with the signal's abort reason. |

After `start()` resolves, two read-only attributes carry the state: `userState` is `"active"` or `"idle"`, and `screenState` is `"locked"` or `"unlocked"`. Both are `null` until the first `start()` has completed, so read them inside the `change` handler or after `await idleDetector.start()`, not synchronously after `new IdleDetector()`.

## Exceptions

The specification defines the following rejections, listed in the order its algorithms check them.

| Method | Exception | Condition |
|---|---|---|
| `requestPermission()` | `InvalidStateError` | The document is not fully active. |
| `requestPermission()` | `NotAllowedError` | The call has no transient user activation; the prompt can only follow a click or key press. |
| `start()` | `InvalidStateError` | The document is not fully active, or the detector's internal state is not `"stopped"` (it is already starting or started). |
| `start()` | `NotAllowedError` | The document is not allowed to use the `idle-detection` policy-controlled feature (default allowlist `'self'`), or the `idle-detection` permission state is `"denied"` when the detector is started. |
| `start()` | `TypeError` | `threshold` is below 60,000 ms. |
| `start()` | the signal's abort reason | `options.signal` is already aborted when `start()` is called, or is aborted while the promise is pending. |

Chromium departs from the specification in one place: when Permissions Policy blocks the feature, `start()` throws a `SecurityError` rather than rejecting with `NotAllowedError` (see the observed callout). Chromium also rejects `start()` with `NotSupportedError` and the message `Idle detection not available.` when the browser-side monitor disconnects, a case the specification does not model.

:::observed
In Chrome, `await IdleDetector.requestPermission()` called outside a user gesture rejects with `NotAllowedError: Must be handling a user gesture to show a permission request.`; inside the gesture the permission bubble reads `<origin> wants to know when you're actively using this device` (English UI). `idleDetector.start({ threshold: 30_000 })` throws `TypeError: Minimum threshold is 1 minute.`, a second `start()` on the same instance throws `InvalidStateError: Idle detector is already started.`, and a denied permission rejects with `NotAllowedError: Idle detection permission denied`. The strings come from Chromium's [`idle_detector.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/idle/idle_detector.cc) (chromium.googlesource.com), [`idle_manager.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/idle/idle_manager.cc) (chromium.googlesource.com) and [`permissions_strings.grdp`](https://chromium.googlesource.com/chromium/src/+/main/components/permissions_strings.grdp) (chromium.googlesource.com).
:::

## Examples

Every example feature-detects the constructor first and falls back to the page's own activity signals (`visibilitychange`, input events) in Firefox and Safari, where `IdleDetector` is undefined.

### Showing an "away" presence badge after one minute of inactivity

The permission request runs inside the `click` handler; the detector itself is started after the promise resolves, which is still within the same task, so no activation is lost. When the API is missing, the fallback marks the user away whenever the tab is hidden, which is the only signal those browsers give you.

```js
const badge = document.querySelector('#presence');
const enable = document.querySelector('#enable-presence');

function markAway(isAway) {
  badge.textContent = isAway ? 'Away' : 'Active';
}

async function startPresence() {
  if (!('IdleDetector' in window)) {
    document.addEventListener('visibilitychange', () => {
      markAway(document.visibilityState === 'hidden');
    });
    return 'fallback';
  }

  const state = await IdleDetector.requestPermission();
  if (state !== 'granted') return 'denied';

  const detector = new IdleDetector();
  detector.addEventListener('change', () => {
    markAway(detector.userState === 'idle' || detector.screenState === 'locked');
  });
  await detector.start({ threshold: 60_000 });
  markAway(false);
  return 'started';
}

enable.addEventListener('click', async () => {
  try {
    enable.hidden = (await startPresence()) !== 'denied';
  } catch (err) {
    console.error(`${err.name}: ${err.message}`);
  }
});
```

`'denied'` keeps the button visible so the user can retry after changing the site setting; a second `requestPermission()` resolves with the stored decision without a new prompt.

### Stopping the detector with an AbortSignal when the user signs out

There is no `stop()` method. The only way to end monitoring is the `AbortSignal` passed to `start()`, which also releases the browser-side monitor. Aborting after `start()` resolved rejects nothing; aborting while `start()` is pending rejects that promise with the abort reason, so the `catch` below distinguishes a deliberate abort from a real failure.

```js
const controller = new AbortController();

async function watchUntilSignOut(detector) {
  try {
    await detector.start({ threshold: 120_000, signal: controller.signal });
  } catch (err) {
    if (err.name === 'AbortError') return; // signed out before start() settled
    throw err;
  }
}

document.querySelector('#sign-out').addEventListener('click', () => {
  controller.abort();
});
```

After `controller.abort()` the detector returns to the `"stopped"` state and the same instance may be started again with a fresh signal; the old `userState` and `screenState` values are no longer updated.

## See also

- [Screen Wake Lock API](/reference/capabilities/wake-lock/), the complementary request to keep the screen from locking
- [Web Locks API](/reference/capabilities/web-locks/), for coordinating work between tabs once a user goes idle
- [PWAs on Firefox](/reference/platforms/firefox/)
- [Idle Detection API: start() method](https://wicg.github.io/idle-detection/#api-idledetector-start) (wicg.github.io)
- [Idle Detection API: requestPermission() method](https://wicg.github.io/idle-detection/#api-idledetector-requestpermission) (wicg.github.io)
- [IdleDetector browser compatibility](https://developer.mozilla.org/en-US/docs/Web/API/IdleDetector#browser_compatibility) (developer.mozilla.org)