Updated New Zealand edition
Latest
Dev Notes

Why Browsers Block window.open and What Works

As browser security around user gestures and popups has evolved, your old school "wait for the fetch to complete then open the popup" pattern may now be directly blocked by the current…

carcino.gen.nz Tech Desk
18 September 20264 min read
A photo of a man browsing a review site for holiday destination reviews while searching for images of holidays on his laptop
Photo: Familydestinationsguide.com Images / Wikimedia Commons, CC BY 2.0
In this story
  1. When window.open Becomes a Forever No-Op
  2. What Counts as a Valid Open
  3. Security Concerns: Open With Noopener, Noreferrer, and No Referrer
  4. What the Features String Can and Can Not Do
  5. Reliable Cross-Browser Patterns
  6. Common Traps
  7. Conclusion

As browser security around user gestures and popups has evolved, your old school "wait for the fetch to complete then open the popup" pattern may now be directly blocked by the current browsers.

Modern browsers can show window.open popups only when they trigger in response to a transient activation — a direct and immediate, separate user gesture or interaction. Browser blocks result when popup calls happen after another action like a fetch or Promise that lets the transient activation (the "click" or "touch") expire.

When window.open Becomes a Forever No-Op

The most common failure is an asynchronous popup: window.open called after an await, inside a promise callback or after a fetch. By the time the promise settles, the click's transient activation may have expired. Chrome and Firefox keep it for a few seconds, so a quick request can get away with it, but Safari is stricter and a slow network breaks it everywhere.

MDN's documentation for window.open notes that browsers block popups that are not opened in response to user activation, such as a click or a key press.

Safari is especially strict: even a short wait inside the click handler, such as an API call, can push window.open outside the gesture.

This pattern gets blocked:

button.addEventListener('click', async () => {
  const data = await fetch('/api/link').then(r => r.json());
  window.open(data.url); // too late: the click's activation may have expired
});

Open the window first, during the click, and point it at the right address once the data arrives:

button.addEventListener('click', async () => {
  const popup = window.open('', '_blank'); // opened inside the gesture
  const data = await fetch('/api/link').then(r => r.json());
  if (popup) popup.location.href = data.url; // then navigate it
  else location.href = data.url;             // blocked anyway: fall back to this tab
});

Do not pass noopener here: with noopener, window.open returns null and you cannot navigate the new window later. If the opened page is not yours, set popup.opener = null before navigating.

What Counts as a Valid Open

If you want to open a popup on a click, then you must also:

  1. Call window.open synchronously inside the handler for a click, tap or key press.
  2. Open the window immediately in the click handler, before any other code runs.
  3. Expect the browser to block it anyway sometimes, and give users a normal link as a fallback.

In other words, open the window first and do the slow work afterwards.

Security Concerns: Open With Noopener, Noreferrer, and No Referrer

Remember for any link — literal or programmatic — that exposes window.opener on the popup, you also enable a form of reverse tabnabbing. So the safest recommendations here are:

<a href="https://sub-page.example.com" target="_blank" rel="noopener noreferrer">link text</a>

A window.open call

let popup = window.open("https://sub-page.example.com",
 "popup", "noopener,noreferrer,width=400,height=360");
if (popup && popup.opener) popup.opener = null;

For the rel="noopener noreferrer" attributes on bare <a target="_blank">:

  • The noopener attribute blocks the created window (the popup) from being able to access the window.opener property and ensures that the popup cannot control the owner window.
  • The noreferrer attribute is the strictest and also blocks the document.referrer property to the target URL, so the popup doesn't inherit the referrer from the source page.

The recent OpenReplay post points out that the noopener rel attribute is now applied automatically on <a target="_blank"> in all major browsers, so you pretty much only need noreferrer if you also want to strip the referrer.

What the Features String Can and Can Not Do

When figuring out the window.open URL, width, height, and feature string, remember:

  • noopener and noreferrer cut the link between the two windows; with either one, window.open returns null.
  • legacy flags like toolbars are probably ignored
  • the exact feature string isn’t cross-browser.

A guide on reverse tabnabbing suggests writing a Window.open helper that always uses "noreferrer,noopener" if you do want to open the full browser window.

Reliable Cross-Browser Patterns

So that’s the key points to remember:

  1. If you want to open a window or tab, open it at the start of the user interaction, then navigate it with location.replace after the fetch completes. Safari is especially sensitive here.
  2. Use <a rel="noopener noreferrer"> whenever you link to a new window. If you let the browser pick the window for you, you get fewer problems.
  3. Avoid opening popups unless you really need a new document context.

Avoiding the popup altogether may be best: many flows work better in the same tab.

For confirmations and small forms, a <dialog> element on the page does the job of a popup without any blocker.

Common Traps

Remember when you debug your browser blocker issues:

  1. Popups don't get randomly blocked: you need a separate user gesture for each popup.
  2. _blank alone does not need noopener, though it still affects the Referrer header.
  3. Awaiting a promise, fetch or timer inside the handler before calling window.open can get the popup blocked.

Conclusion

The usual causes of a blocked window.open call are:

  • trying to open it asynchronously, after a response completes
  • missing transient activation due to waiting for other APIs
  • not placing the call directly in the user gesture handler

Open the window at the very start of the handler, then fetch and navigate it. If you cannot, show a normal link once the data is ready and let the user's second click open it.

Read next