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.
How it works
Section titled “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
Section titled “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, 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
Section titled “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, 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 four gating levels
Section titled “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
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.
Which APIs need which state
Section titled “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 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.
Examples
Section titled “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
Section titled “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, 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.
See also
Section titled “See also”- HTML Standard: Tracking user activation (html.spec.whatwg.org)
- User activation (developer.mozilla.org)
- UserActivation (developer.mozilla.org)
- Web Share API
- Async Clipboard API
- Document.requestStorageAccess()
Specifications
| Specification | Status |
|---|---|
| None. | |