The User Activation API

As a web developer, you’ve probably noticed that certain APIs only work if an end-user clicks or taps on an HTML element. For example, if you try to run the following code in Safari’s Web Inspector, it will result in an error:

await navigator.share({ text: "hi" });
NotAllowedError: The request is not allowed by the user agent or 
the platform in the current context, possibly because the user denied permission.

This error happens when code is not run as a direct result of the end-user clicking or tapping on an HTML element (e.g., a <button>).

Having code that runs as a result of an end-user action is what the HTML specification refers to as “user activation”. There are a large number of APIs on the web that depend on user activation. Common ones include:

  • window.open()
  • navigator.share()
  • navigator.wakelock.request()
  • PaymentRequest.prototype.show()
  • and there are many, many more…

So what constitutes a “user activation”?

The HTML spec defines the following events as “activation triggering user events”:

Together, this list effectively constitutes “user activation”. You’ll note the list of events above is really small. It’s restricted so that certain calls to APIs can only happen as a result of those very distinct end-user actions. This prevents end-users from being accidentally (or deliberately!) spammed with popup windows or other intrusive browser dialogs.

Now that we know about these special events, we can now write code to take into account user activation:

button.addEventListener("click", async () => {
   // This works fine...
   await navigator.share({text: "hi"});
});

So even though we are not specifically listening for a "mousedown" event, we know that the activation triggering event has occurred, so our code can run without throwing any errors.

End-user protections

Now you might be wondering, can one run execute multiple commands that require user activation insingle or multiple event listeners? Consider the following code sample:

button.addEventListener("click", async () => {

   // This works fine...
   await navigator.share({text: "hi"});

   // This will fail...
   window.open("https://example.com");
});

button.addEventListener("click", async () => {
   // This will now fail too...
   window.open("https://example.com");
});

But why do the calls to window.open() fail there? To understand that, we need to delve deeper into how browsers handle user activation under the hood.

Meet “transient” and “sticky” activation

When an “activation triggering user event” occurs what actually happens is that the browser starts an internal timer specifically tied to a browser tab. This timer is not directly exposed to the web page and runs for a short time (a few seconds, maybe). Each browser engine can determine how much time is allocated and it can change for a number of reasons (i.e., it’s deliberately not observable by JavaScript!). It’s designed to give your code enough time to perform some particular task (e.g., it could process some image data and then call navigator.share() to share the image with another application).

In HTML, this timer is called transient activation. And while this timer is running, that browser window “has transient activation”. HTML also defines a concept called sticky activation. This simply means that the web page has had transient activation at some point in the past.

Although rare, some APIs (e.g., Web Audio) use sticky activation to perform some actions.

Now, the above doesn’t explain why window.open() failed above. To understand why, we need to now discuss what HTML calls “activation-consuming APIs”.

APIs that “consume” the user activation

As the name suggests, “activation-consuming APIs” consume the user activation. That is, when those APIs are called, they effectively reset the transient activation timer, so the web page no longer has transient activation.

This behavior is why window.open() fails above: calling navigator.share() consumed the user activation, meaning that window.open() no longer had transient activation (so it fails).

A list of common APIs that consume transient activation in WebKit:

  • Web Notification’s requestPermission() method.
  • Payment Request: the show() method.
  • And, as we have already discussed, Web Share’s share() method.

This list is not exhaustive, and new APIs are being added to the web all the time that either rely on or consume transient activation.

As a point of interest: not all APIs consume the user activation. Some only require transient activation but won’t consume it. That allows multiple asynchronous operations dependent on user activation to take place. Otherwise, it would require the user to click or press on a button over and over again to complete a task, which would be quite annoying for them.

Scope of transient activation

A really useful thing to know about transient activation is that it’s scoped to the entire window (or browser tab)! That means that, so long as all iframes on a page are same-origin, they all have transient activation. However, for security reasons, cross-origin iframes will not have transient activation.

Transient activation across all same origin iframes

For third-party iframes to have transient activation, a user must explicitly activate an HTML element inside the third-party iframe. However, once they activate an element then transient activation propagates to the parent and to any iframes that are same origin to iframe where the activation took place:

Activation propagating to parent frame, to other iframes that match the third-party iframe

Security Note: you can (and should!) restrict what capacities third-party iframes have access to by setting the allow= and/or sandbox= attributes, as needed.

The UserActivation API

To assist developers with dealing with user activation, the HTML standard introduces a simple API to check if a page has transient and/or sticky activation.

  • navigator.userActivation.isActive:
    Returns true when the window has transient activation.
  • navigator.userActivation.hasBeenActive:
    Returns true if the window has had transient activation in the past (i.e., “sticky activation”).

So for example, you can do something like:

if (navigator.userActivation.isActive) {
    await navigator.share({text: "hi"})
}

Limitations and ongoing standards work

There are two significant limitations with the current user activation model that standards folks are still grappling with.
Firstly, consider the following case, where a file takes too long to download and the transient activation timer runs out:

button.onclick = () => {
    // Slow network + really big file
    const image = await fetch("really-big-file");

    // Oh no!!! transient activation expired! 😢
    navigator.share({files: [image]});
}   

There are ongoing discussions at the WHATWG and W3C about how we might address the problem above. Unfortunately, we don’t yet have a solution, but naturally we need some means to extend the transient activation so the code above doesn’t fail.

Secondly, there are legitimate use cases for enabling transient activation in a third-party iframe from a first-party document (e.g., to allow a third-party to process a request for payment). There is ongoing discussions to see if there is some means to safely enable third-party iframes to also have transient activation in special cases.

Automation and testing

To help developers deal with tricky edge-cases that could arise from the transient activation unexpectedly expiring, WebKit has been working with other browser vendors to allow the user activation to be consumed via Web Driver.

Conclusion

Web APIs being gated on user activation helps keep the user safe from annoying intrusions, like multiple popup windows or notification spam, while allowing developers to do the right thing in response to user interaction. The UserActivation API can help you determine if it’s OK to call a function that depends on user activation.

You can try out the User Activation API in Safari Technology Preview release 160 or later.