# User activation and the APIs it gates

> Sticky and transient activation in the HTML Standard: which input events grant them, which APIs consume them, and what navigator.userActivation reads.

User activation is the window state the HTML Standard uses to allow APIs "only when the user is actively interacting with the web page or has interacted with the page at least once", so that a script cannot open a popup or vibrate a phone unprompted. The standard defines sticky activation, transient activation and history-action activation; transient activation can expire after a user-agent-defined duration.

## How it works

Activation is tracked per `Window`, as timestamps rather than as a flag, which is what lets one mechanism express both "has ever interacted" and "interacted a moment ago".

### The data model in the HTML Standard

Each `Window` carries a last activation timestamp, which is "either a `DOMHighResTimeStamp`, positive infinity (indicating that W has never been activated), or negative infinity (indicating that the activation has been consumed)", initially positive infinity, plus a last history-action activation timestamp. The user agent also defines a transient activation duration, "a constant number indicating how long a user activation is available for certain user activation-gated APIs", which the specification expects to be at most a few seconds "so that the user can possibly perceive the link between an interaction with the page and the page calling the activation-gated API" ([HTML Standard: Tracking user activation](https://html.spec.whatwg.org/multipage/interaction.html), html.spec.whatwg.org).

Sticky activation holds when the current high resolution time is greater than or equal to the last activation timestamp; the specification describes it as the window's historical activation state, which "starts false, then changes to true (and never changes back to false) when W gets the very first activation notification". Transient activation holds when that time is also less than the last activation timestamp plus the transient activation duration, and the state is "considered expired if it becomes false because the transient activation duration time has elapsed since the last user activation". It can also become false before the expiry time through an activation consumption.

### Which events grant activation

An activation triggering input event is any event whose `isTrusted` attribute is true and whose type is one of: `keydown`, provided the key is neither the Esc key nor a shortcut key reserved by the user agent; `mousedown`; `pointerdown`, provided the event's `pointerType` is `mouse`; `pointerup`, provided the event's `pointerType` is not `mouse`; or `touchend`. MDN's catalogue adds that the behaviour of excluded keys "may vary by browser" and names Caps Lock, Num Lock and Print Screen as keys that do not produce user activation on some keyboards ([User activation](https://developer.mozilla.org/en-US/docs/Web/Security/User_activation), developer.mozilla.org).

The notification and the consumption cover different sets of browsing contexts, and the specification says so deliberately: "an activation consumption changes (to false) the transient activation states for all browsing contexts in the page, but an activation notification changes (to true) the states for a subset of those browsing contexts", because the exhaustive consumption "prevents malicious sites from making multiple calls to an activation consuming API from a single user activation (possibly by exploiting a deep hierarchy of `iframe`s)".

### The four gating levels

The specification classifies activation-dependent APIs into four levels. Sticky activation-gated APIs require sticky activation, "so they are blocked until the very first user activation". Transient activation-gated APIs require transient activation but do not consume it, "so multiple calls are allowed per user activation until the transient state expires". Transient activation-consuming APIs require transient activation and consume it on each call. History-action activation-consuming APIs require the history-action activation state and consume it on each call, and unlike transient activation "there is no time limit within which such APIs must be used".

### Browser support for the UserActivation interface

BCD `api.UserActivation` records support from Chrome 72, Firefox 120 and Safari 16.4, mirrored into the Chromium-based browsers the record lists (Edge, Opera, Samsung Internet and WebView) and into Chrome Android, Firefox Android and Safari on iOS ([api/UserActivation.json](https://github.com/mdn/browser-compat-data/blob/main/api/UserActivation.json), github.com). The same versions apply to both members, `hasBeenActive` and `isActive`.

## Which APIs need which state

MDN marks both of its lists as not exhaustive. The APIs it lists as requiring transient activation include `Navigator.share()`, `Window.open()`, `PaymentRequest.show()`, `Element.requestFullscreen()`, `MediaDevices.getDisplayMedia()`, `Document.requestStorageAccess()`, `Clipboard.read()`, `Clipboard.write()`, `USB.requestDevice()`, `Serial.requestPort()`, `HID.requestDevice()`, `Window.showOpenFilePicker()`, `Window.showSaveFilePicker()`, `Window.showDirectoryPicker()`, `EyeDropper.open()`, `Clients.openWindow()` and `WindowClient.focus()`. The APIs it lists as requiring sticky activation include the `beforeunload` event, `Navigator.vibrate()`, `VirtualKeyboard.show()`, autoplay of the Media and Web Audio APIs (in particular for `AudioContext`s), and `clipboardchange` events, which MDN notes can also be enabled by the user granting the `clipboard-read` permission.

The practical consequence of the split is when the call can run. A sticky-gated call works any time after the visitor's first interaction with the page, so a confirmation dialog on unload is fine. A transient-gated call can run while transient activation remains active, until its duration elapses or a consuming API consumes it for all browsing contexts in the page.

## Examples

The first example reads the sticky and transient states; the second calls a transient-gated API while activation is available.

### Reading the two activation states

`navigator.userActivation` exposes `hasBeenActive`, which "indicates whether the window has sticky user activation", and `isActive`, which "indicates whether the window has transient user activation" ([UserActivation](https://developer.mozilla.org/en-US/docs/Web/API/UserActivation), developer.mozilla.org). Because the interface arrived at different versions in each engine, the read needs a detection branch.

```js
function activationState() {
  if (!('userActivation' in navigator)) {
    // Interface absent: branch on the event that fired instead of the window state.
    return { hasBeenActive: null, isActive: null, readable: false };
  }
  const { hasBeenActive, isActive } = navigator.userActivation;
  return { hasBeenActive, isActive, readable: true };
}
```

A `readable: false` result is the signal to stop asking the window: call the gated API while activation is expected to remain available, and handle the rejection it produces when the activation is not there. Note that `click` is not itself on the list above — the activation triggering input events are `keydown`, `mousedown`, the conditional `pointerdown`/`pointerup` pair and `touchend`. A trusted `click` normally follows one of them, which is why its handler is a reasonable place to make the call, but following one is not the same as being one, so the rejection path has to exist either way.

### Calling a consuming API while activation is available

Web Share is one of the transient activation-consuming APIs, so `share()` must be called while transient activation is still present. An asynchronous step is a problem only if activation expires or another API consumes it before the call.

```js
shareButton.addEventListener('click', async () => {
  if (!('share' in navigator)) {
    showCopyLinkDialog();
    return;
  }
  const payload = { title: document.title, url: location.href };
  try {
    await navigator.share(payload);
  } catch (error) {
    // NotAllowedError when the activation was missing or already consumed; a
    // dismissed share sheet rejects too. Neither is a bug to rethrow.
    if (error.name === 'NotAllowedError') showCopyLinkDialog();
  }
});
```

Building `payload` from values already in hand calls `share()` promptly after the click, and the `catch` is the rejection path that covers the activation being gone anyway. An awaited operation can still return while activation is active, but the transient duration can elapse during it; another consuming API can also consume the activation first.

:::observed
Chromium's `NavigatorShare::share()` makes the gate visible in source. It calls `LocalFrame::ConsumeTransientUserActivation(window->GetFrame())` directly in the condition. When that returns false, it logs `Share without transient activation (user gesture)` at `VLOG(1)` and throws a `NotAllowedError` whose message is the literal string `Must be handling a user gesture to perform a share request.` ([navigator_share.cc](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/webshare/navigator_share.cc), chromium.googlesource.com).
:::

## See also

- [HTML Standard: Tracking user activation](https://html.spec.whatwg.org/multipage/interaction.html) (html.spec.whatwg.org)
- [User activation](https://developer.mozilla.org/en-US/docs/Web/Security/User_activation) (developer.mozilla.org)
- [UserActivation](https://developer.mozilla.org/en-US/docs/Web/API/UserActivation) (developer.mozilla.org)
- [Web Share API](/reference/capabilities/web-share/)
- [Async Clipboard API](/reference/capabilities/clipboard/)
- [Document.requestStorageAccess()](/reference/storage/storage-access-api/)