Skip to main content

How to Work with Popups and Window Methods in JavaScript

Opening new browser windows and popups from JavaScript is a technique that has been part of the web since the earliest days of the language. While popups have earned a bad reputation due to years of abuse by advertisers, they remain a legitimate tool for specific use cases: OAuth authentication flows, payment gateway redirects, print previews, and auxiliary interfaces that need to run alongside the main application.

Modern browsers impose strict restrictions on when and how popups can be opened, and understanding these restrictions is essential for building features that work reliably. This guide covers the window.open() method and its parameters, how popup blockers work and how to avoid triggering them, techniques for communicating between the opener window and the popup, and how to manage popup lifecycle with window.close() and window.closed.

window.open(url, name, params) for Opening Popups

The window.open() method opens a new browser window or tab. It accepts three arguments, all optional:

const newWindow = window.open(url, name, params);
  • url: The URL to load in the new window. If omitted or an empty string, a blank page (about:blank) opens.
  • name: A name for the new window. If a window with this name already exists, the URL opens in that window instead of creating a new one.
  • params: A comma-separated string of window features (size, position, UI elements). If omitted, the browser opens a normal new tab.

Opening a Simple Popup

// Open a new tab (default behavior when no params specified)
window.open('https://example.com');

// Open a blank popup window
const popup = window.open('', '', 'width=400,height=300');

// Open a specific URL in a sized popup
const popup = window.open(
'https://example.com/help',
'helpWindow',
'width=600,height=400'
);

The params String

The third argument controls the appearance and behavior of the popup window. It is a comma-separated string of key=value pairs with no spaces:

const popup = window.open(
'https://example.com',
'myPopup',
'width=800,height=600,left=100,top=100,scrollbars=yes,resizable=yes'
);

Common parameters:

ParameterDescriptionExample
widthWindow width in pixelswidth=600
heightWindow height in pixelsheight=400
leftDistance from the left edge of the screenleft=200
topDistance from the top edge of the screentop=100
menubarShow the menu barmenubar=yes
toolbarShow the navigation toolbartoolbar=yes
locationShow the address barlocation=yes
statusShow the status barstatus=yes
resizableAllow resizingresizable=yes
scrollbarsShow scrollbars if content overflowsscrollbars=yes

Values can be yes/no or 1/0 for boolean features.

note

Modern browsers largely ignore most of the UI-related parameters (menubar, toolbar, status, location) for security reasons. The address bar, for example, is almost always shown regardless of the location parameter, because hiding it would make phishing attacks easier. The parameters that reliably work across browsers are width, height, left, and top.

Tab vs. Popup Window

The browser decides whether to open a new tab or a popup window based on the params argument:

  • No params (or params is empty/omitted): The browser opens a new tab.
  • params is specified with size/position features: The browser opens a popup window.
// Opens in a new tab
window.open('https://example.com');
window.open('https://example.com', 'myWindow');

// Opens as a popup window (because params specify dimensions)
window.open('https://example.com', 'myPopup', 'width=600,height=400');

This distinction matters because users and browsers treat tabs and popups differently. Tabs feel native and expected, while popup windows can be jarring and are more likely to be blocked.

Centering a Popup on Screen

A common requirement is centering the popup on the user's screen:

function openCenteredPopup(url, name, width, height) {
const left = (screen.width - width) / 2;
const top = (screen.height - height) / 2;

const params = [
`width=${width}`,
`height=${height}`,
`left=${left}`,
`top=${top}`,
'scrollbars=yes',
'resizable=yes'
].join(',');

return window.open(url, name, params);
}

// Usage
const popup = openCenteredPopup('https://example.com/login', 'login', 500, 600);

The name Parameter

The name parameter serves as an identifier for the window. If you call window.open() with a name that matches an already open window, the URL loads in that existing window instead of opening a new one:

// First call: opens a new window named "details"
window.open('https://example.com/item/1', 'details', 'width=600,height=400');

// Second call with same name: reuses the existing window
window.open('https://example.com/item/2', 'details', 'width=600,height=400');
// The "details" window navigates to item/2

Special name values:

NameBehavior
_blankAlways opens a new window/tab (default)
_selfOpens in the current window
_parentOpens in the parent frame
_topOpens in the topmost frame
// Always opens a new window, even if called multiple times
window.open('https://example.com', '_blank', 'width=400,height=300');

The Return Value

window.open() returns a reference to the new window object, or null if the popup was blocked:

const popup = window.open('https://example.com', '', 'width=400,height=300');

if (popup === null) {
console.log('Popup was blocked!');
} else {
console.log('Popup opened successfully');
// You can interact with the popup through this reference
}

Always check for null before using the returned reference. Popup blockers can silently prevent the window from opening.

Popup blockers are built into every modern browser. They exist because popups were historically abused to show unsolicited advertisements, trick users into clicking malicious content, and create frustrating experiences. Understanding how popup blockers work is essential for building features that rely on new windows.

The Fundamental Rule: User Gesture Required

Browsers allow window.open() only when it is called as a direct result of a user action (a click, a keyboard event, etc.). If JavaScript tries to open a popup without a user gesture, the browser blocks it.

// ✅ Works: called directly inside a click handler
button.addEventListener('click', () => {
window.open('https://example.com', '', 'width=600,height=400');
});

// ❌ Blocked: called on page load (no user gesture)
window.addEventListener('load', () => {
window.open('https://example.com'); // Blocked!
});

// ❌ Blocked: called from a timer (user gesture has expired)
button.addEventListener('click', () => {
setTimeout(() => {
window.open('https://example.com'); // Likely blocked!
}, 3000); // 3 seconds later (too long)
});

The User Gesture Window

Browsers give you a short time window after a user gesture during which window.open() is allowed. The exact duration varies by browser (typically a few seconds at most), but the safest approach is to call window.open() synchronously within the event handler:

// ✅ Safest: synchronous call in the handler
button.addEventListener('click', () => {
const popup = window.open('https://example.com');
});

// ⚠️ Risky: short async delay might work in some browsers
button.addEventListener('click', async () => {
const data = await fetch('/api/get-url'); // Short network call
const { url } = await data.json();
const popup = window.open(url); // May be blocked in strict browsers
});

The "Open First, Navigate Later" Pattern

When you need to fetch data before knowing which URL to open, the safest pattern is to open the window immediately (preserving the user gesture) and navigate it afterward:

button.addEventListener('click', async () => {
// Open immediately with the user gesture: this won't be blocked
const popup = window.open('about:blank', 'myPopup', 'width=600,height=400');

if (!popup) {
alert('Please allow popups for this site');
return;
}

// Optionally show a loading message in the popup
popup.document.write('<p>Loading...</p>');

try {
// Now fetch the actual URL
const response = await fetch('/api/get-redirect-url');
const { url } = await response.json();

// Navigate the already-open popup to the real URL
popup.location.href = url;
} catch (error) {
popup.document.write('<p>Error loading content. Please close this window.</p>');
}
});

This pattern is widely used in OAuth flows where you need to call an API to get the authorization URL before opening it.

Detecting Blocked Popups

Always check whether the popup was actually opened:

function openPopupSafely(url, name, params) {
const popup = window.open(url, name, params);

if (!popup || popup.closed || typeof popup.closed === 'undefined') {
// Popup was blocked
return null;
}

// Some browsers open the popup but immediately close it
// Check again after a short delay
setTimeout(() => {
if (popup.closed) {
console.warn('Popup was opened then immediately closed by the browser');
}
}, 1000);

return popup;
}

// Usage
button.addEventListener('click', () => {
const popup = openPopupSafely(
'https://example.com/auth',
'auth',
'width=500,height=600'
);

if (!popup) {
showFallbackMessage('Popup blocked. Please allow popups and try again.');
// Or offer an alternative: navigate in the same window
}
});

Guiding Users to Allow Popups

When a popup is blocked, provide clear guidance:

function openWithFallback(url, name, params) {
const popup = window.open(url, name, params);

if (!popup) {
// Show a user-friendly message
const message = document.createElement('div');
message.innerHTML = `
<div style="position:fixed;top:0;left:0;right:0;padding:16px;
background:#fff3cd;border-bottom:2px solid #ffc107;z-index:10000;
text-align:center;font-family:sans-serif;">
<strong>Popup blocked!</strong> Please allow popups for this site, then
<a href="${url}" target="_blank" rel="noopener">click here to continue</a>.
<button onclick="this.parentElement.remove()" style="margin-left:16px;cursor:pointer;">
Dismiss
</button>
</div>
`;
document.body.appendChild(message.firstElementChild);
return null;
}

return popup;
}
tip

As a general rule, avoid relying on popups for critical application flows. If a popup is essential (like an OAuth login), always provide a fallback path such as redirecting in the same window. Many users have popup blockers enabled and may not know how to allow popups for your site.

Communicating Between Windows

When you open a popup from your page, JavaScript provides mechanisms for the opener and the popup to communicate with each other. The level of access depends on whether both windows are on the same origin (same protocol, domain, and port).

Same-Origin Communication

If both windows are on the same origin, they have full access to each other's window objects, DOM, and JavaScript variables.

From the opener, accessing the popup:

const popup = window.open('/popup.html', 'myPopup', 'width=400,height=300');

// Wait for the popup to load
popup.addEventListener('load', () => {
// Access the popup's document
popup.document.body.innerHTML = '<h1>Hello from opener!</h1>';

// Call a function defined in the popup
popup.someFunction('data from opener');

// Read a variable from the popup
console.log(popup.someVariable);
});

From the popup, accessing the opener:

Every popup window has a window.opener property that references the window that opened it:

<!-- popup.html -->
<script>
// Access the opener window
console.log('Opened by:', window.opener.location.href);

// Call a function in the opener
window.opener.handlePopupResult({ status: 'success', token: 'abc123' });

// Modify the opener's DOM
window.opener.document.getElementById('status').textContent = 'Authenticated!';
</script>

Practical Example: OAuth-Style Flow

A common pattern where the popup sends a result back to the opener:

Main page (opener):

let authPopup = null;

// Function that the popup will call when authentication completes
function onAuthComplete(result) {
console.log('Authentication result:', result);

if (result.success) {
document.getElementById('user-name').textContent = result.userName;
document.getElementById('login-btn').style.display = 'none';
}

if (authPopup && !authPopup.closed) {
authPopup.close();
}
}

document.getElementById('login-btn').addEventListener('click', () => {
authPopup = window.open(
'/auth/login',
'authPopup',
'width=500,height=600'
);

if (!authPopup) {
// Fallback: redirect in the same window
window.location.href = '/auth/login?redirect=' + encodeURIComponent(window.location.href);
}
});

Popup page (/auth/login):

<script>
// After successful authentication...
function completeAuth(userName, token) {
if (window.opener && !window.opener.closed) {
// Send result back to the opener
window.opener.onAuthComplete({
success: true,
userName: userName,
token: token
});
} else {
// Opener is gone: redirect to the main page
window.location.href = '/?token=' + token;
}
}
</script>

Polling the Popup

Sometimes you cannot modify the popup's code (e.g., it is a third-party OAuth page). In this case, you can poll the popup's URL to detect when it redirects back to your domain:

function openAuthPopup(authUrl, redirectUrl) {
return new Promise((resolve, reject) => {
const popup = window.open(authUrl, 'auth', 'width=500,height=600');

if (!popup) {
reject(new Error('Popup blocked'));
return;
}

const interval = setInterval(() => {
try {
// This throws a cross-origin error until the popup
// redirects back to our domain
if (popup.closed) {
clearInterval(interval);
reject(new Error('Popup closed by user'));
return;
}

// When the popup redirects to our redirect URL,
// we can access its location (same origin)
if (popup.location.href.startsWith(redirectUrl)) {
const url = new URL(popup.location.href);
const code = url.searchParams.get('code');

clearInterval(interval);
popup.close();
resolve(code);
}
} catch (e) {
// Cross-origin error: popup is still on the third-party domain
// This is expected, keep polling
}
}, 500);
});
}

// Usage
document.getElementById('login-btn').addEventListener('click', async () => {
try {
const code = await openAuthPopup(
'https://auth-provider.com/authorize?client_id=xxx&redirect_uri=https://mysite.com/callback',
'https://mysite.com/callback'
);
console.log('Got auth code:', code);
await exchangeCodeForToken(code);
} catch (error) {
console.error('Auth failed:', error.message);
}
});

Cross-Origin Communication with postMessage

When the opener and popup are on different origins, direct DOM access is blocked by the same-origin policy. The postMessage API provides a safe way to communicate across origins:

Opener (on https://mysite.com):

const popup = window.open('https://other-site.com/page', 'cross', 'width=500,height=400');

// Send a message to the popup
popup.postMessage({ action: 'requestData' }, 'https://other-site.com');

// Listen for messages from the popup
window.addEventListener('message', (event) => {
// Always verify the origin!
if (event.origin !== 'https://other-site.com') {
return; // Ignore messages from unexpected origins
}

console.log('Received from popup:', event.data);
});

Popup (on https://other-site.com):

// Listen for messages from the opener
window.addEventListener('message', (event) => {
// Verify the origin
if (event.origin !== 'https://mysite.com') {
return;
}

if (event.data.action === 'requestData') {
// Send data back to the opener
event.source.postMessage(
{ result: 'Here is your data' },
'https://mysite.com'
);
}
});
warning

Always verify event.origin when receiving messages through postMessage. Without this check, any website could send messages to your window and potentially trick your code into performing unintended actions. Never use '*' as the target origin in production code when sending sensitive data.

The window.opener Security Concern

When you open a link with target="_blank" (or window.open), the opened page has access to window.opener. On same-origin pages, this means the popup can manipulate the opener's DOM. On cross-origin pages, the popup can still navigate the opener using window.opener.location.

This is a security risk known as reverse tabnapping: a malicious popup could redirect the opener to a phishing page:

// Malicious popup code:
window.opener.location = 'https://fake-mysite.com/login';
// The user's original tab now shows a phishing page

To prevent this, use rel="noopener" on links or set opener to null:

<!-- For HTML links -->
<a href="https://example.com" target="_blank" rel="noopener noreferrer">Open</a>
// For window.open: modern browsers support the 'noopener' feature
const popup = window.open('https://example.com', '_blank', 'noopener');
// Note: with 'noopener', window.open returns null (no reference to the popup)

// Alternative: nullify opener after opening
const popup = window.open('https://example.com');
if (popup) {
popup.opener = null; // The popup can no longer access the opener
}
caution

Using noopener with window.open() means the return value is null, so you cannot communicate with or control the popup afterward. Only use noopener when you do not need a reference to the opened window.

window.close() and window.closed

Closing a Popup

The window.close() method closes a window. For security reasons, it only works on windows that were opened by JavaScript using window.open(). You cannot close the user's main browser tab or a window the user opened manually:

// Close a popup from the opener
const popup = window.open('https://example.com', 'myPopup', 'width=400,height=300');

// Later...
popup.close(); // Closes the popup
// Close the current window from within the popup
// (only works if this window was opened by window.open)
window.close();
// ❌ This will NOT close the user's main tab
// The browser ignores or warns: "Scripts may close only the windows that were opened by them."
window.close(); // Does nothing in the main window

Self-Closing Popup

A popup that closes itself after completing its task:

<!-- callback.html - the redirect target after OAuth -->
<script>
// Extract the auth code from the URL
const params = new URLSearchParams(window.location.search);
const code = params.get('code');

// Send result to opener
if (window.opener && !window.opener.closed) {
window.opener.postMessage(
{ type: 'auth-complete', code: code },
window.location.origin
);
}

// Close this popup
window.close();
</script>

Checking If a Window Is Still Open

The window.closed property returns true if the window has been closed. This is useful for monitoring the popup's state:

const popup = window.open('/processing', 'processor', 'width=400,height=300');

// Check periodically if the popup is still open
const checkInterval = setInterval(() => {
if (!popup || popup.closed) {
clearInterval(checkInterval);
console.log('Popup was closed');
handlePopupClosed();
}
}, 500);

Managing Popup Lifecycle

Here is a complete pattern for managing a popup with proper cleanup:

class PopupManager {
constructor() {
this.popup = null;
this.checkInterval = null;
this.messageHandler = null;
}

open(url, name, params) {
// Close any existing popup
this.close();

this.popup = window.open(url, name, params);

if (!this.popup) {
throw new Error('Popup was blocked by the browser');
}

// Monitor the popup
this.checkInterval = setInterval(() => {
if (this.popup.closed) {
this.cleanup();
this.onClose();
}
}, 500);

// Listen for messages
this.messageHandler = (event) => {
if (event.source === this.popup) {
this.onMessage(event.data);
}
};
window.addEventListener('message', this.messageHandler);

return this.popup;
}

close() {
if (this.popup && !this.popup.closed) {
this.popup.close();
}
this.cleanup();
}

cleanup() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
if (this.messageHandler) {
window.removeEventListener('message', this.messageHandler);
this.messageHandler = null;
}
this.popup = null;
}

focus() {
if (this.popup && !this.popup.closed) {
this.popup.focus();
}
}

get isOpen() {
return this.popup !== null && !this.popup.closed;
}

// Override these in subclasses or instances
onMessage(data) {
console.log('Message from popup:', data);
}

onClose() {
console.log('Popup was closed');
}
}

// Usage
const authManager = new PopupManager();

authManager.onMessage = (data) => {
if (data.type === 'auth-success') {
console.log('Authenticated:', data.token);
authManager.close();
}
};

authManager.onClose = () => {
console.log('User closed the auth popup');
};

document.getElementById('login-btn').addEventListener('click', () => {
try {
authManager.open('/auth/start', 'auth', 'width=500,height=600');
} catch (error) {
alert(error.message);
}
});

window.focus() and window.blur()

You can bring a popup to the front or send it to the back:

const popup = window.open('https://example.com', 'myPopup', 'width=400,height=300');

// Bring the popup to the front
popup.focus();

// Send the popup to the back (rarely useful)
popup.blur();

focus() is particularly useful when reusing a named window. If the popup is already open but behind other windows, calling focus() brings it forward:

function openOrFocusPopup(url, name, params) {
// Try to reuse the existing window
const popup = window.open(url, name, params);

if (popup) {
popup.focus(); // Bring to front if it was already open
}

return popup;
}

Moving and Resizing Popups

JavaScript can move and resize popup windows it has opened. These methods only work on windows created with window.open():

const popup = window.open('', 'myPopup', 'width=300,height=200,left=100,top=100');

// Move the popup
popup.moveTo(500, 300); // Move to screen coordinates (500, 300)
popup.moveBy(50, 0); // Move 50px to the right

// Resize the popup
popup.resizeTo(600, 400); // Set to 600x400 pixels
popup.resizeBy(100, 50); // Increase width by 100px, height by 50px
note

Many modern browsers restrict or ignore moveTo, moveBy, resizeTo, and resizeBy calls. Some browsers only allow these on windows opened by window.open() with explicit size parameters. Do not rely on these methods for critical functionality.

Scrolling a Popup

// Scroll the popup's content
popup.scrollTo(0, 500); // Scroll to position
popup.scrollBy(0, 100); // Scroll by offset

When to Use Popups (and When Not To)

Legitimate Use Cases

  • OAuth / third-party authentication: Opening a login page for Google, GitHub, etc. in a popup, receiving the auth token via postMessage or URL redirect.
  • Payment gateways: Some payment processors require opening their checkout in a separate window.
  • Print previews: Opening a formatted print view that the user can print and close.
  • Auxiliary tools: Color pickers, file browsers, or help windows that need to stay alongside the main application.

When to Use Alternatives

For most cases, popups are not the best choice. Modern alternatives include:

// Instead of a popup for a form or dialog:
// Use a modal dialog
const dialog = document.getElementById('my-dialog');
dialog.showModal(); // Native <dialog> element

// Instead of a popup for additional content:
// Use a sidebar, drawer, or expandable panel

// Instead of a popup for notifications:
// Use the Notification API
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
new Notification('New message', { body: 'You have a new message' });
}
});

Popups disrupt the user's workflow, can be blocked, and behave inconsistently across browsers and devices (especially mobile, where popups often open as new tabs). Use them only when the interaction genuinely requires a separate window.

Summary

Working with popups and window methods in JavaScript requires understanding browser security restrictions and user experience implications:

  • window.open(url, name, params) opens a new window or tab. The params string controls size and position. Without params, browsers open a new tab. The method returns a reference to the new window or null if blocked.
  • Popup blockers require a direct user gesture (click, keypress) for window.open() to succeed. Open the window synchronously within the event handler. If you need async data first, open about:blank immediately and navigate later.
  • Same-origin windows have full mutual access through window.opener (popup to opener) and the returned window reference (opener to popup). Cross-origin windows must use window.postMessage() for safe communication, always verifying event.origin.
  • window.close() only works on windows opened by window.open(). Use window.closed to check if a popup is still open. Always clean up intervals, event listeners, and references when a popup closes.
  • Security: use rel="noopener" or noopener in window.open() params when you do not need a reference to prevent reverse tabnapping. Always validate postMessage origins.
  • Prefer modern alternatives (modal dialogs, <dialog> element, in-page UI) over popups whenever possible. Reserve popups for authentication flows, payment redirects, and cases that genuinely require a separate window.