JavaScript Cross-Window Communication
Learn cross-window communication in JavaScript: postMessage, the same-origin policy, window references, localStorage events, and the Broadcast Channel API.
Cross-window communication is the exchange of data between separate browsing contexts — a parent page and a popup it opened, a page and an embedded iframe, or two tabs from the same site. The browser deliberately isolates these contexts for security, so they cannot freely read each other's variables or DOM. Instead, JavaScript offers a small set of well-defined channels for passing messages between them.
This chapter covers when you need cross-window communication, the same-origin policy that governs it, and four practical mechanisms: postMessage(), direct window references, storage events, and the Broadcast Channel API. It builds on window.open() and popups; if you are new to the browser model, start with the browser environment overview.
Understanding Cross-Window Communication
A browsing context is anything with its own window object: a tab, a popup, or an iframe. Two contexts can reach each other only through a controlled API, and how much they are allowed to do depends on their origin — the combination of protocol, host, and port (for example https://www.w3docs.com:443).
The same-origin policy
The same-origin policy (SOP) is the rule that decides what one context may do to another:
- Same origin (identical protocol, host, and port): contexts can read each other's DOM directly and call
postMessage()freely. - Cross-origin (any part differs): direct DOM access is blocked. The only sanctioned channel is
postMessage(), which the receiver must validate.
This is why postMessage() is the recommended approach almost everywhere: it works the same whether the windows share an origin or not, and it forces you to be explicit about who you trust.
When you need it
- Popup windows. A window opened with
window.open()often needs to send results back to the page that launched it (an OAuth login popup, a file picker). - Iframes. Embedded widgets — payment forms, maps, third-party players — exchange data with the host page.
- Tabs and other contexts. Two tabs of the same app may need to stay in sync (a logout in one tab should log out the others).
Methods of Cross-Window Communication
Using window.postMessage()
The window.postMessage() method is the safest, most portable way to send data across windows or frames — it works for both same-origin and cross-origin contexts. The sender calls targetWindow.postMessage(data, targetOrigin), and the receiver listens for a message event.
Two arguments matter for security:
targetOrigin(the second argument topostMessage) restricts who may receive the message. Pass the exact origin you expect ('https://example.com'); use the wildcard'*'only when the data is not sensitive, since any window at that target can then read it.event.origin(on the receiver) tells you who sent the message. Always check it before trustingevent.data— this is how you reject messages from untrusted pages.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Cross-Window Communication</title>
<style>
#childIframe, #childPopup {
width: 100%;
height: 200px;
border: 1px solid black;
margin-top: 20px;
}
</style>
</head>
<body>
<h1>Cross-Window Communication Examples</h1>
<!-- Button to Open Popup -->
<button id="openPopup">Open Popup</button>
<div id="parentPopupDisplay"></div>
<!-- Iframe -->
<iframe id="childIframe" srcdoc="
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>Child Iframe</title>
</head>
<body>
<div id='childIframeDisplay'></div>
<script>
window.addEventListener('message', (event) => {
// Note: For cross-origin contexts, replace window.location.origin with the hardcoded parent origin.
if (event.origin !== window.location.origin) return;
document.getElementById('childIframeDisplay').innerText = 'Message from parent: ' + event.data;
event.source.postMessage('Hello, Parent Window!', event.origin);
});
</script>
</body>
</html>
"></iframe>
<div id="iframeDisplay"></div>
<!-- Scripts for Parent Window -->
<script>
// Handle Popup Communication
document.getElementById('openPopup').addEventListener('click', () => {
const popup = window.open('', 'popupWindow', 'width=600,height=400');
popup.document.write(`
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>Popup Window</title>
</head>
<body>
<div id='popupDisplay'></div>
<script>
window.addEventListener('message', (event) => {
// Note: For cross-origin contexts, replace window.location.origin with the hardcoded parent origin.
if (event.origin !== window.location.origin) return;
document.getElementById('popupDisplay').innerText = 'Message from parent: ' + event.data;
event.source.postMessage('Hello, Parent Window!', event.origin);
});
<\/script>
</body>
</html>
`);
setTimeout(() => {
// For cross-origin, replace '*' with the exact target origin (e.g., 'https://example.com')
popup.postMessage('Hello from parent!', '*');
}, 1000);
});
// Handle Iframe Communication
const iframe = document.getElementById('childIframe');
iframe.onload = () => {
iframe.contentWindow.postMessage('Hello from parent window!', '*');
};
window.addEventListener('message', (event) => {
if (event.origin !== window.location.origin) return;
if (event.source === iframe.contentWindow) {
document.getElementById('iframeDisplay').innerText = 'Message from iframe: ' + event.data;
} else {
document.getElementById('parentPopupDisplay').innerText = 'Message from popup: ' + event.data;
}
});
</script>
</body>
</html>In this combined example, the parent window opens a popup and embeds an iframe. Both the popup and the iframe can communicate with the parent window using postMessage(). Messages are displayed within respective div elements for clear visibility.
While document.write() works for simple demos, modern best practices recommend using DOMParser or Blob URLs to inject content into popups safely.
Accessing Window References
When you open a new window with window.open(), the return value is a reference to that window. The opened window, in turn, can reach back to its opener through window.opener, and a parent can reach an iframe through iframe.contentWindow. These direct references work only when both contexts share the same origin — otherwise the SOP throws a security error. Use them for tightly coupled, same-origin pages; reach for postMessage() whenever an origin boundary is involved.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Direct Manipulation Example</title>
<style>
#childIframe {
width: 100%;
height: 200px;
border: 1px solid black;
margin-top: 20px;
}
</style>
</head>
<body>
<h1>Direct Manipulation Example</h1>
<!-- Button to Open Popup -->
<button id="openChild">Open Child Window</button>
<div id="parentChildDisplay"></div>
<!-- Iframe -->
<iframe id="childIframe" srcdoc="
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>Child Iframe</title>
</head>
<body>
<div id='childIframeContent'>Initial Content</div>
</body>
</html>
"></iframe>
<!-- Scripts for Parent Window -->
<script>
document.getElementById('openChild').addEventListener('click', () => {
const childWindow = window.open('', 'childWindow', 'width=600,height=400');
childWindow.document.write(`
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>Child Window</title>
</head>
<body>
<div id='childContent'>Initial Content</div>
</body>
</html>
`);
// Ensure the content is updated after the window has fully loaded
setTimeout(() => {
childWindow.document.body.innerHTML += '<p>Message from parent window</p>';
}, 1000); // Adjust the timeout duration as necessary
});
const iframe = document.getElementById('childIframe');
iframe.onload = () => {
const iframeDoc = iframe.contentWindow.document;
iframeDoc.getElementById('childIframeContent').innerText += ' - Updated by Parent Window';
};
</script>
</body>
</html>In this example, the parent window opens a child window and directly modifies its content once it has loaded. Additionally, it updates the content of an embedded iframe.
Direct DOM manipulation via contentWindow.document or window.opener is restricted by the Same-Origin Policy (SOP) for cross-origin contexts. For secure and reliable communication, always prefer postMessage(). For same-origin popups, window.opener can be used as an alternative to access the parent window directly.
When using srcdoc, the iframe content loads asynchronously. The onload handler ensures the DOM is ready, but for complex scenarios, consider triggering communication via a DOMContentLoaded event dispatched from within the iframe.
Using Local Storage and Session Storage
localStorage is shared by every same-origin tab and window, and writing to it fires a storage event in all other contexts. That makes it a simple way to broadcast a change between tabs without any direct window reference. (sessionStorage is per-tab and does not propagate, so it is not useful for cross-tab messaging.) For a deeper look at the storage objects themselves, see localStorage and sessionStorage.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Local Storage Example</title>
</head>
<body>
<h1>Local Storage Example</h1>
<button id="storeData">Store Data</button>
<button id="retrieveData">Retrieve Data</button>
<div id="storageDisplay"></div>
<script>
// Listen for changes triggered by other windows/tabs
window.addEventListener('storage', (event) => {
if (event.key === 'sharedData') {
document.getElementById('storageDisplay').innerText = 'Updated Data: ' + event.newValue;
}
});
document.getElementById('storeData').addEventListener('click', () => {
localStorage.setItem('sharedData', 'This is shared data');
});
document.getElementById('retrieveData').addEventListener('click', () => {
const data = localStorage.getItem('sharedData');
document.getElementById('storageDisplay').innerText = 'Stored Data: ' + data;
});
</script>
</body>
</html>In this example, the parent window stores data in localStorage and retrieves it upon button clicks. To enable cross-window synchronization, a storage event listener is added. Note that the storage event only fires in other browsing contexts, not the one that triggered the change.
Broadcast Channel API
The Broadcast Channel API is the purpose-built tool for same-origin messaging between tabs, windows, and iframes. Any context that opens a channel with the same name receives every message posted to it — no window references and no storage-event workarounds. It cannot cross origins, so for third-party iframes you still need postMessage().
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Broadcast Channel Example</title>
</head>
<body>
<h1>Broadcast Channel Example</h1>
<button id="sendMessage">Send Message</button>
<div id="broadcastDisplay"></div>
<script>
const channel = new BroadcastChannel('example_channel');
channel.onmessage = (event) => {
document.getElementById('broadcastDisplay').innerText = 'Broadcast message received: ' + event.data;
};
document.getElementById('sendMessage').addEventListener('click', () => {
channel.postMessage('Hello from another context!');
});
</script>
</body>
</html>In this example, a Broadcast Channel is created, and a message is sent when the button is clicked. The message is received and displayed within a div element.
To test this example properly:
- Click the 'Try it Yourself' button twice, to have the example page in two different tabs.
- Then, click the "Send Message" button in one of the tabs/windows.
- You should see the message appear in the other tab/window.
The BroadcastChannel API is designed for inter-tab communication, so the message will be sent from one tab/window to all others that are open to the same origin (the same HTML file in this case).
Choosing the right method
| Method | Cross-origin? | Best for |
|---|---|---|
postMessage() | Yes | The default. Popups and third-party iframes, anywhere an origin boundary exists. |
| Direct window references | No (same-origin only) | Tightly coupled same-origin popups/iframes you fully control. |
storage event | No (same-origin only) | Broadcasting state changes to other tabs with no extra API. |
| Broadcast Channel | No (same-origin only) | Clean many-to-many messaging between same-origin tabs and frames. |
When in doubt, use postMessage() — it is the only method that works across origins and the only one with a built-in security model.
Best practices
Serialize complex data as JSON. postMessage() uses the structured clone algorithm and can pass objects directly, but explicit JSON keeps the contract clear and works with storage events (which only carry strings):
const message = { type: 'greeting', content: 'Hello, Child Window!' };
// JSON.stringify produces: {"type":"greeting","content":"Hello, Child Window!"}
childWindow.postMessage(JSON.stringify(message), '*');Handle closed or inaccessible targets. A popup may be closed by the user, and a cross-origin window will throw if you touch it directly. Guard your calls:
if (childWindow && !childWindow.closed) {
try {
childWindow.postMessage('Hello, Child Window!', '*');
} catch (e) {
console.error('Failed to send message:', e);
}
}Always validate the sender. On the receiving side, check event.origin against an allow-list before acting on event.data, and never eval() an incoming message.
Conclusion
Cross-window communication in JavaScript is a powerful feature that, when used correctly, can significantly enhance the interactivity and user experience of web applications. By employing methods like window.postMessage(), local storage, and the Broadcast Channel API, developers can efficiently manage data exchange between different windows, tabs, and frames. Follow best practices to ensure secure and robust communication, and leverage the provided examples to integrate these techniques into your projects.