Skip to content

Concept in Capabilities

User activation and the APIs it gates

Published

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.

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”.

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, 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.

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, 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 iframes)”.

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

Section titled “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, github.com). The same versions apply to both members, hasBeenActive and isActive.

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 AudioContexts), 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.

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

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, developer.mozilla.org). Because the interface arrived at different versions in each engine, the read needs a detection branch.

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

Section titled “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.

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.

Specifications

SpecificationStatus
None.