Understanding window.open() Behavior on iOS Safari

Recently, I dealt with a strange bug around PDF downloads.

The functionality was simple: The user fills out a form, clicks a button, and a PDF opens in a new browser tab.

This was working as expected for every browser and every device, except Safari on iOS.

  • Safari on macOS: Works
  • Chrome on iOS: Works
  • Safari on iOS: Fails

Some digging led me to realize that Safari on iOS enforces stricter pop-up rules than other browsers. It only allows pop-ups that are triggered directly by a user action.

Here’s a simplified version of the original code:

At first glance, this seems fine. The user clicks a button, and window.open() is called.

The issue comes from where the window is opened. The window.open() is inside an asynchronous callback. By the time it runs, the browser no longer considers it a direct user action.

This difference in how browsers interpret user activation is because of something called ‘transient activation’. When the user interacts with the page, like when they click on a button, the browser starts a short timer that allows the page to initiate certain actions. This timer defines whether a browser sees an action as user-initiated.

Different browsers handle this differently. Firefox and Chrome have purposely extended this timer. Safari is stricter about the transient activation timer, and Safari on iOS is even stricter still. This means any small delay, such as waiting for a Promise to resolve, is enough to miss this window in Safari on iOS.

The exact time limit for each browser is not publicized, and the behavior of pop-up blocking in each browser is intentionally opaque, but we can approximate how long each browser allows pop-ups with a quick test.

In this CodePen example, there are three buttons. They each open a pop-up after a short delay.

You can open this example in different browsers to see the different behavior in each: https://codepen.io/malformedBubble/pen/vENYKYR

These were my results:

.5 seconds

1 second

5 seconds

Chrome

Yes

Yes

No

Firefox

Yes

Yes

No

Safari iOS

Yes

No

No

Safari macOS

Yes

No

No

A simple way to work around this is to call window.open() at the beginning of the function, before any other code runs.

Here’s the updated version:

Now, window.open() is called while transient activation is still active, and Safari sees that it has been called by a direct user action. Once the PDF is ready, we can redirect the new tab to our blob URL.

This one-line fix allows our code to work in every browser, even those with strict rules on pop-ups.

author avatar
Sarah Kenny

Related posts